Query handlers in CQRS are surprisingly difficult to test effectively without coupling them to the exact shape of your read models.

Imagine you have a ProductCatalog bounded context. Your command side handles CreateProduct and UpdateProductPrice. Your query side exposes a GetProductDetails query that returns a DTO like this:

{
  "productId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
  "name": "Wireless Mouse",
  "description": "Ergonomic wireless mouse with long battery life.",
  "currentPrice": 25.99,
  "categoryName": "Electronics",
  "tags": ["wireless", "computer", "accessory"]
}

A common temptation is to mock the repository that GetProductDetailsHandler uses. But this is a trap. If you mock the repository, you’re not testing if your handler can actually shape the data from the read model into the DTO. You’re testing if your handler can call a mocked method.

Let’s see this in action. We’ll use C# with xUnit for testing.

First, our query handler and its dependencies:

// The DTO our query returns
public class ProductDetailsDto
{
    public Guid ProductId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal CurrentPrice { get; set; }
    public string CategoryName { get; set; }
    public List<string> Tags { get; set; }
}

// The query itself
public class GetProductDetailsQuery : IRequest<ProductDetailsDto>
{
    public Guid ProductId { get; set; }
}

// The handler
public class GetProductDetailsQueryHandler : IRequestHandler<GetProductDetailsQuery, ProductDetailsDto>
{
    private readonly IReadModelRepository _repository;

    public GetProductDetailsHandler(IReadModelRepository repository)
    {
        _repository = repository;
    }

    public async Task<ProductDetailsDto> Handle(GetProductDetailsQuery request, CancellationToken cancellationToken)
    {
        // This is the crucial part: fetching from the read model
        var readModel = await _repository.GetAsync<ProductReadModel>(request.ProductId);

        if (readModel == null)
        {
            return null; // Or throw an exception
        }

        // And then mapping it to the DTO
        return new ProductDetailsDto
        {
            ProductId = readModel.Id,
            Name = readModel.ProductName,
            Description = readModel.ProductDescription,
            CurrentPrice = readModel.Price,
            CategoryName = readModel.CategoryName,
            Tags = readModel.ProductTags.Split(',').ToList() // Example transformation
        };
    }
}

// Simplified read model entity and repository interface
public class ProductReadModel
{
    public Guid Id { get; set; }
    public string ProductName { get; set; }
    public string ProductDescription { get; set; }
    public decimal Price { get; set; }
    public string CategoryName { get; set; }
    public string ProductTags { get; set; } // Stored as comma-separated string
}

public interface IReadModelRepository
{
    Task<TReadModel> GetAsync<TReadModel>(Guid id) where TReadModel : class;
}

Now, the wrong way to test this:

[Fact]
public async Task GetProductDetails_WhenProductExists_ReturnsCorrectDto_MockedRepo()
{
    // Arrange
    var productId = Guid.NewGuid();
    var mockRepo = new Mock<IReadModelRepository>();
    var expectedReadModel = new ProductReadModel
    {
        Id = productId,
        ProductName = "Wireless Mouse",
        ProductDescription = "Ergonomic mouse.",
        Price = 25.99m,
        CategoryName = "Electronics",
        ProductTags = "wireless,computer"
    };

    mockRepo.Setup(r => r.GetAsync<ProductReadModel>(productId))
            .ReturnsAsync(expectedReadModel);

    var handler = new GetProductDetailsQueryHandler(mockRepo.Object);
    var query = new GetProductDetailsQuery { ProductId = productId };

    // Act
    var result = await handler.Handle(query, CancellationToken.None);

    // Assert
    Assert.NotNull(result);
    Assert.Equal(productId, result.ProductId);
    Assert.Equal("Wireless Mouse", result.Name);
    // ... and so on for all properties
    Assert.Equal(new List<string> { "wireless", "computer" }, result.Tags); // This assertion is testing the handler's mapping logic, which is good.
}

This test looks good, but it doesn’t verify that the ProductReadModel as it exists in the database/cache can be correctly transformed. It only verifies that if the repository returned that specific ProductReadModel object, the handler would map it correctly. The real risk is that the read model changes (e.g., ProductTags becomes a JSON array in the database, or ProductDescription is renamed to Description) and your handler’s mapping logic breaks, but your tests still pass because you’re mocking the old structure.

The correct approach involves a lightweight, in-memory repository that simulates the actual read model data. This forces your handler to interact with data that is shaped like your real read model, not a mocked abstraction.

Here’s how you’d do it:

// A simple in-memory repository for testing
public class InMemoryReadModelRepository : IReadModelRepository
{
    private readonly Dictionary<Guid, object> _data = new Dictionary<Guid, object>();

    public void Add<TReadModel>(Guid id, TReadModel model) where TReadModel : class
    {
        _data[id] = model;
    }

    public Task<TReadModel> GetAsync<TReadModel>(Guid id) where TReadModel : class
    {
        if (_data.TryGetValue(id, out var model))
        {
            return Task.FromResult(model as TReadModel);
        }
        return Task.FromResult<TReadModel>(null);
    }
}

[Fact]
public async Task GetProductDetails_WhenProductExists_ReturnsCorrectDto_InMemoryRepo()
{
    // Arrange
    var productId = Guid.NewGuid();
    var inMemoryRepo = new InMemoryReadModelRepository();

    // This is the key: we seed the repository with a ProductReadModel
    // that closely mirrors what would be stored.
    var storedReadModel = new ProductReadModel
    {
        Id = productId,
        ProductName = "Wireless Mouse",
        ProductDescription = "Ergonomic mouse.",
        Price = 25.99m,
        CategoryName = "Electronics",
        ProductTags = "wireless,computer,ergonomic" // Note the extra tag
    };
    inMemoryRepo.Add(productId, storedReadModel);

    var handler = new GetProductDetailsQueryHandler(inMemoryRepo);
    var query = new GetProductDetailsQuery { ProductId = productId };

    // Act
    var result = await handler.Handle(query, CancellationToken.None);

    // Assert
    Assert.NotNull(result);
    Assert.Equal(productId, result.ProductId);
    Assert.Equal("Wireless Mouse", result.Name);
    Assert.Equal("Ergonomic mouse.", result.Description);
    Assert.Equal(25.99m, result.CurrentPrice);
    Assert.Equal("Electronics", result.CategoryName);
    // Crucially, we test the mapping logic, including the string split.
    Assert.Equal(new List<string> { "wireless", "computer", "ergonomic" }, result.Tags);
}

[Fact]
public async Task GetProductDetails_WhenProductDoesNotExist_ReturnsNull_InMemoryRepo()
{
    // Arrange
    var productId = Guid.NewGuid();
    var inMemoryRepo = new InMemoryReadModelRepository(); // No data added

    var handler = new GetProductDetailsQueryHandler(inMemoryRepo);
    var query = new GetProductDetailsQuery { ProductId = productId };

    // Act
    var result = await handler.Handle(query, CancellationToken.None);

    // Assert
    Assert.Null(result);
}

The difference is subtle but profound. Instead of mocking IReadModelRepository, we create an instance of InMemoryReadModelRepository and populate it with a ProductReadModel. This ProductReadModel instance is the data that the handler will conceptually "read." When the handler calls _repository.GetAsync<ProductReadModel>(request.ProductId), it gets back this exact ProductReadModel object. The handler then performs its mapping logic. Any changes to the structure of ProductReadModel (e.g., if ProductTags was an IEnumerable<string> directly in the read model) would require updating the storedReadModel instantiation in the test, and if the handler’s mapping logic didn’t keep up, the test would fail. This is exactly what you want: tests that break when the underlying read model contract changes.

This pattern ensures your query handlers are tested against data shaped like your actual read models, catching regressions in the mapping logic or unexpected data transformations. The InMemoryReadModelRepository acts as a lightweight, deterministic stand-in for your actual data store (like a document database or a relational table optimized for queries).

The most surprising thing about this approach is that it doesn’t require a full database setup for your query handler tests. You’re not testing the persistence layer; you’re testing the transformation from a read-model shape to a DTO shape, using a testable representation of that read-model shape. The InMemoryReadModelRepository is essentially a collection of typed objects, and your handler’s job is to fetch one and reshape it.

The next challenge you’ll face is testing handlers that need to aggregate data from multiple read models.

Want structured learning?

Take the full Cqrs course →