Unit Testing CQRS Handlers With Moq, Fluent Assertions, and xUnit
CQRS keeps your business logic isolated in small, single-purpose handler classes. That isolation is exactly what makes handlers trivial to…
Unit Testing CQRS Handlers With Moq, Fluent Assertions, and xUnit

CQRS keeps your business logic isolated in small, single-purpose handler classes. That isolation is exactly what makes handlers trivial to unit-test. This guide wires up xUnit, Moq, and Fluent Assertions to give every handler a full happy-path and sad-path test suite — no integration infrastructure required.
TL;DR
Create one xUnit test project. Install Moq and FluentAssertions. Mock every dependency your handler receives via its constructor. Call Handle() directly. Assert the result with Fluent Assertions. Verify Moq expectations. That is the entire pattern — no web host, no database.
The Problem
Most teams write MediatR handlers and then test them by spinning up a full WebApplicationFactory or hitting a real database. That makes tests slow, brittle, and hard to run in CI without connection strings. Handlers are plain C# classes — they need no HTTP pipeline and no EF Core context to be tested. Mock the repository, call Handle(), assert the result.
Architecture Overview
The test project mirrors your application layer:
Application/ → Queries/GetProductById/ → GetProductByIdQuery.cs, GetProductByIdHandler.cs
Application/ → Commands/CreateProduct/ → CreateProductCommand.cs, CreateProductHandler.cs
Application/ → Interfaces/IProductRepository.cs
Tests/ → Handlers/GetProductByIdHandlerTests.cs
Tests/ → Handlers/CreateProductHandlerTests.cs
Step 1 — Project Setup & NuGet
Create a dedicated xUnit test project and add a project reference to your Application layer. Then install the three testing libraries from the CLI:
File: CLI
dotnet new xunit -n MyApp.Tests
dotnet add MyApp.Tests reference src/MyApp.Application
dotnet add MyApp.Tests package Moq
dotnet add MyApp.Tests package FluentAssertions
dotnet add MyApp.Tests package MediatR
Step 2 — The CQRS Contracts
Define the query, command, result DTO, and the repository interface. The handler under test depends on these contracts — Moq will implement the interface at runtime.
File: Application/Interfaces/IProductRepository.cs
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id,
CancellationToken ct = default);
Task<int> AddAsync(Product product,
CancellationToken ct = default);
Task<bool> ExistsBySkuAsync(string sku,
CancellationToken ct = default);
}
File: Application/Queries/GetProductById/GetProductByIdQuery.cs
public record GetProductByIdQuery(int Id)
: IRequest<ProductDto>;
public record ProductDto(int Id, string Name, string Sku);
File: Application/Commands/CreateProduct/CreateProductCommand.cs
public record CreateProductCommand(string Name, string Sku)
: IRequest<int>;
Step 3 — The Handlers Under Test
These are the production classes. They accept the repository through their constructor, which is exactly the seam Moq exploits to swap in a fake implementation during tests.
File: Application/Queries/GetProductById/GetProductByIdHandler.cs
public class GetProductByIdHandler
: IRequestHandler<GetProductByIdQuery, ProductDto>
{
private readonly IProductRepository _repo;
public GetProductByIdHandler(IProductRepository repo)
=> _repo = repo;
public async Task<ProductDto> Handle(
GetProductByIdQuery request,
CancellationToken ct)
{
var product = await _repo.GetByIdAsync(request.Id, ct);
if (product is null)
throw new NotFoundException(
$"Product {request.Id} not found.");
return new ProductDto(product.Id, product.Name, product.Sku);
}
}
File: Application/Commands/CreateProduct/CreateProductHandler.cs
public class CreateProductHandler
: IRequestHandler<CreateProductCommand, int>
{
private readonly IProductRepository _repo;
public CreateProductHandler(IProductRepository repo)
=> _repo = repo;
public async Task<int> Handle(
CreateProductCommand request,
CancellationToken ct)
{
bool exists = await _repo.ExistsBySkuAsync(request.Sku, ct);
if (exists)
throw new ConflictException(
$"SKU '{request.Sku}' already exists.");
var product = new Product
{
Name = request.Name,
Sku = request.Sku
};
return await _repo.AddAsync(product, ct);
}
}
Step 4 — Query Handler Test (Happy Path)
Arrange: create a Mock<IProductRepository>, set up GetByIdAsync to return a known product. Act: instantiate the handler and call Handle. Assert with Fluent Assertions that the returned DTO maps every field correctly.
File: Tests/Handlers/GetProductByIdHandlerTests.cs
public class GetProductByIdHandlerTests
{
private readonly Mock<IProductRepository> _repoMock;
private readonly GetProductByIdHandler _handler;
public GetProductByIdHandlerTests()
{
_repoMock = new Mock<IProductRepository>();
_handler = new GetProductByIdHandler(_repoMock.Object);
}
[Fact]
public async Task Handle_ProductExists_ReturnsMappedDto()
{
// Arrange
var product = new Product { Id = 1, Name = "Widget", Sku = "WGT-01" };
_repoMock
.Setup(r => r.GetByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(product);
var query = new GetProductByIdQuery(1);
// Act
var result = await _handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Id.Should().Be(product.Id);
result.Name.Should().Be(product.Name);
result.Sku.Should().Be(product.Sku);
_repoMock.Verify(
r => r.GetByIdAsync(1, It.IsAny<CancellationToken>()),
Times.Once);
}
}
Step 5 — Command Handler Test (Happy Path)
Mock ExistsBySkuAsync to return false (no duplicate). Mock AddAsync to return a new ID. Verify the handler returns that ID and calls AddAsync exactly once.
File: Tests/Handlers/CreateProductHandlerTests.cs
public class CreateProductHandlerTests
{
private readonly Mock<IProductRepository> _repoMock;
private readonly CreateProductHandler _handler;
public CreateProductHandlerTests()
{
_repoMock = new Mock<IProductRepository>();
_handler = new CreateProductHandler(_repoMock.Object);
}
[Fact]
public async Task Handle_ShouldReturnNewId_WhenSkuIsUnique()
{
// Arrange
_repoMock
.Setup(r => r.ExistsBySkuAsync("WGT-01", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
_repoMock
.Setup(r => r.AddAsync(It.IsAny<Product>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(42);
var command = new CreateProductCommand
{
Name = "Widget",
Sku = "WGT-01"
};
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().Be(42);
_repoMock.Verify(
r => r.AddAsync(It.IsAny<Product>(), It.IsAny<CancellationToken>()),
Times.Once);
}
}
Step 6 — Testing the Sad Path
When ExistsBySkuAsync returns true, the handler should throw a DuplicateSkuException. Assert the exception type and message. Verify that AddAsync is never called.
File: Tests/Handlers/CreateProductHandlerTests.cs
[Fact]
public async Task Handle_ShouldThrow_WhenSkuIsDuplicate()
{
// Arrange
_repoMock
.Setup(r => r.ExistsBySkuAsync("WGT-01", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
var command = new CreateProductCommand
{
Name = "Widget",
Sku = "WGT-01"
};
// Act
Func<Task> act = async () =>
await _handler.Handle(command, CancellationToken.None);
// Assert
await act.Should()
.ThrowAsync<DuplicateSkuException>()
.WithMessage("*WGT-01*");
_repoMock.Verify(
r => r.AddAsync(It.IsAny<Product>(), It.IsAny<CancellationToken>()),
Times.Never);
}
NuGet Packages
Install all required packages in one shot from the Package Manager Console.
# xUnit test runner
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Microsoft.NET.Test.Sdk
# Moq — mocking framework
dotnet add package Moq
# FluentAssertions
dotnet add package FluentAssertions
# MediatR
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
Wrapping Up
You now have a fully test-driven CQRS layer. Every handler is decoupled from infrastructure, every behaviour is verified in isolation, and every edge case has a name. Your CI pipeline will thank you.
If this saved you time, hit Follow for the next article in this series. Drop your questions in the comments — I read them all.
Next Steps
- Add integration tests using an in-memory database to validate the full MediatR pipeline end-to-end.
- Wire validation into your pipeline with FluentValidation and a MediatR IPipelineBehavior.
- Explore snapshot testing for complex query results to catch regressions fast.
- Coming next: Validating CQRS Commands With FluentValidation and MediatR Pipeline Behaviours.
메타데이터
- post_id
- fb3fec8d3869
- slug
- unit-testing-cqrs-handlers-with-moq-fluent-assertions-and-xunit-fb3fec8d3869
- url
- https://medium.com/@maged_/unit-testing-cqrs-handlers-with-moq-fluent-assertions-and-xunit-fb3fec8d3869
- canonical_url
- https://medium.com/@maged_/unit-testing-cqrs-handlers-with-moq-fluent-assertions-and-xunit-fb3fec8d3869
- author_url
- https://medium.com/@maged_
- status
- ok
- fetched_at
- 2026-06-11 15:16:29