DEV Community

Cover image for A Complete Guide to Unit Testing in .NET Core (with xUnit, Moq, and FluentAssertions)
Morteza Jangjoo
Morteza Jangjoo

Posted on

A Complete Guide to Unit Testing in .NET Core (with xUnit, Moq, and FluentAssertions)

A Complete Guide to Unit Testing in .NET Core

Unit testing is one of the most important steps in building reliable, maintainable software.

In this article, we’ll walk through how to write unit tests in .NET Core using xUnit, Moq, and FluentAssertions — step by step — with a real example of a simple blog application.


1. Creating the Test Project

Let's assume you have an existing API project called BlogApp.

We’ll create a test project that validates the behavior of a service class called PostService.

dotnet new xunit -n BlogApp.Tests
dotnet add BlogApp.Tests reference ../BlogApp/BlogApp.csproj
Enter fullscreen mode Exit fullscreen mode

📁 Folder structure:

BlogApp/
 ┣ Controllers/
 ┣ Services/
 ┣ Models/
 ┣ BlogApp.csproj
BlogApp.Tests/
 ┣ PostServiceTests.cs
 ┣ BlogApp.Tests.csproj
Enter fullscreen mode Exit fullscreen mode

2. Installing Dependencies

We’ll use a few popular testing libraries:

dotnet add package Moq
dotnet add package FluentAssertions
Enter fullscreen mode Exit fullscreen mode

What they do:

  • Moq → Helps create fake (mocked) dependencies to isolate your test.
  • FluentAssertions → Makes your test assertions readable and expressive.

3. The Service We’re Going to Test

Here’s our PostService class that fetches posts from a repository:

public interface IPostRepository
{
    Task<Post> GetByIdAsync(int id);
    Task<List<Post>> GetAllAsync();
}

public class PostService
{
    private readonly IPostRepository _repo;

    public PostService(IPostRepository repo)
    {
        _repo = repo;
    }

    public async Task<Post> GetPostByIdAsync(int id)
    {
        if (id <= 0)
            throw new ArgumentException("Invalid ID");

        var post = await _repo.GetByIdAsync(id);
        if (post == null)
            throw new KeyNotFoundException("Post not found");

        return post;
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Writing Unit Tests with xUnit, Moq, and FluentAssertions

Let’s write three tests to cover valid and invalid cases.

File: BlogApp.Tests/PostServiceTests.cs

using Xunit;
using Moq;
using System.Threading.Tasks;
using System.Collections.Generic;
using FluentAssertions;

public class PostServiceTests
{
    [Fact]
    public async Task GetPostByIdAsync_ShouldReturnPost_WhenIdIsValid()
    {
        // Arrange
        var mockRepo = new Mock<IPostRepository>();
        mockRepo.Setup(r => r.GetByIdAsync(1))
                .ReturnsAsync(new Post { Id = 1, Title = "Test Post" });

        var service = new PostService(mockRepo.Object);

        // Act
        var result = await service.GetPostByIdAsync(1);

        // Assert
        result.Should().NotBeNull();
        result.Title.Should().Be("Test Post");
    }

    [Fact]
    public async Task GetPostByIdAsync_ShouldThrowArgumentException_WhenIdIsInvalid()
    {
        var mockRepo = new Mock<IPostRepository>();
        var service = new PostService(mockRepo.Object);

        Func<Task> act = async () => await service.GetPostByIdAsync(0);
        await act.Should().ThrowAsync<ArgumentException>()
            .WithMessage("Invalid ID");
    }

    [Fact]
    public async Task GetPostByIdAsync_ShouldThrowKeyNotFoundException_WhenPostNotFound()
    {
        var mockRepo = new Mock<IPostRepository>();
        mockRepo.Setup(r => r.GetByIdAsync(2))
                .ReturnsAsync((Post)null);

        var service = new PostService(mockRepo.Object);

        Func<Task> act = async () => await service.GetPostByIdAsync(2);
        await act.Should().ThrowAsync<KeyNotFoundException>()
            .WithMessage("Post not found");
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Running the Tests

To execute all tests, simply run:

dotnet test
Enter fullscreen mode Exit fullscreen mode

Expected output:

Starting test execution...
Passed!  - 3 passed, 0 failed
Enter fullscreen mode Exit fullscreen mode

Best Practices for Unit Testing

Keep tests isolated — never depend on real databases or APIs.
Use descriptive test names like GetPostByIdAsync_ShouldReturnPost_WhenIdIsValid.
Make tests deterministic — they should pass or fail consistently.
Integrate tests in CI/CD pipelines (e.g., GitHub Actions, Azure DevOps).
Use FluentAssertions for expressive, readable test code.


Bonus Tip — When to Use Integration Tests

Unit tests validate small pieces of logic in isolation.
If you need to verify end-to-end functionality (like API + database + repository), write integration tests instead.


Summary

Unit testing is not just about code coverage — it’s about confidence.
Using xUnit, Moq, and FluentAssertions, you can easily write clean, maintainable tests that make your .NET Core applications more reliable and easier to maintain.


Recommended Resources


get sample code from github

Author

I’m Morteza Jangjoo and “Explaining things I wish someone had explained to me.”
🌐 Follow me on Hashnode or GitHub


💬 If you found this helpful, give it a ❤️ and share your favorite testing tip in the comments!

Top comments (0)