← Back to list

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…

Adam · 2026-05-30 18:01 · 0 claps · 3.9 min read paywalled
#software-development #software-engineering #programming #dotnet #csharp
Open on Medium ↗
Wiki topics: 💻 · Programming 💭 · Philosophy of Spirit

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