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
📁 Folder structure:
BlogApp/
┣ Controllers/
┣ Services/
┣ Models/
┣ BlogApp.csproj
BlogApp.Tests/
┣ PostServiceTests.cs
┣ BlogApp.Tests.csproj
2. Installing Dependencies
We’ll use a few popular testing libraries:
dotnet add package Moq
dotnet add package FluentAssertions
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;
}
}
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");
}
}
5. Running the Tests
To execute all tests, simply run:
dotnet test
Expected output:
Starting test execution...
Passed! - 3 passed, 0 failed
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
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)