A Complete Guide to Writing Unit Tests in .NET Core (with xUnit + Moq)

Morteza Jangjoo, Senior .NET Backend Developer with 15+ years of experience in C#, ASP.NET Core, SQL Server, and Microservices. Skilled in building scalable, high-performance systems.
Unit testing is one of the most important parts of modern software development.
In this article, you’ll learn how to write unit tests in .NET Core using xUnit, Moq, and FluentAssertions, step-by-step — with a real example from a blog app.
1. Creating a Test Project in .NET Core
Let’s say we already have a Web API project called BlogApp.
We want to write unit tests for a service class named PostService.
To create a new test project, run the following commands:
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. Install Required Packages
Run these commands inside the test project:
dotnet add package Moq
dotnet add package FluentAssertions
Explanation:
Moq → for mocking dependencies like repositories or external services
FluentAssertions → for cleaner and more expressive assertions
3. The Code We’re Going to Test
Here’s our target class in the main project:
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
Now, let’s create PostServiceTests.cs in the BlogApp.Tests project:
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()
{
// Arrange
var mockRepo = new Mock<IPostRepository>();
var service = new PostService(mockRepo.Object);
// Act
Func<Task> act = async () => await service.GetPostByIdAsync(0);
// Assert
await act.Should().ThrowAsync<ArgumentException>()
.WithMessage("Invalid ID");
}
[Fact]
public async Task GetPostByIdAsync_ShouldThrowKeyNotFoundException_WhenPostNotFound()
{
// Arrange
var mockRepo = new Mock<IPostRepository>();
mockRepo.Setup(r => r.GetByIdAsync(2))
.ReturnsAsync((Post)null);
var service = new PostService(mockRepo.Object);
// Act
Func<Task> act = async () => await service.GetPostByIdAsync(2);
// Assert
await act.Should().ThrowAsync<KeyNotFoundException>()
.WithMessage("Post not found");
}
}
5. Running the Tests
To run your tests, simply execute:
dotnet test
Expected output:
Starting test execution...
Passed! - 3 passed, 0 failed
All tests are passing — great!
Best Practices for Writing Unit Tests
Each test must be independent.
Use clear and descriptive test names.
Example:GetPostByIdAsync_ShouldReturnPost_WhenIdIsValidKeep tests fast and deterministic.
Mock all external dependencies (databases, APIs, file systems).
Integrate tests into CI/CD pipelines to ensure code quality continuously.
Bonus: When to Use Integration Tests Instead
Unit tests are perfect for testing small logic components,
but for end-to-end scenarios (e.g., API endpoints, DB queries), use Integration Tests instead.
Summary
By using xUnit, Moq, and FluentAssertions, you can write clean, powerful, and maintainable tests in .NET Core.
Unit testing helps you catch bugs early, increase confidence, and build reliable software faster.




