Should I test that?

Should I test CQRS handlers?

Verdict

Yes

Test each business rule in a CQRS command handler by sending the command through the mediator, with an in-memory fake repository or a test database, and asserting the saved state; do not write handler tests that only check which mocked methods the handler called.

Why

Test CQRS command handlers, because the write-side business rules run there. The typical case is a MediatR command handler in an ASP.NET Core API that loads an aggregate, applies a rule such as a pickup cutoff, and saves. Blast radius is users and Change frequency is regularly, because customers see a broken rule and handlers change with most features. Detectability is eventually, because a broken rule saves a plausible state without an error. Reversibility is with-effort, because wrong records need a repair script, and Test cost is moderate, because each test needs a fake repository or a test database.

When the decision changes
WhenDecisionWhy
The handler is a query handler that loads one read-model row by its ID and maps it to a DTOTest minimally: one test against the production database engineReversibility falls to trivial, because a lookup by ID stores nothing
The command handler charges a card or issues a refundTest mandatory: the main path, every known failure and the boundary amountsBlast radius rises to money and Reversibility to costly
A query handler must filter records by tenantTest mandatory: seed another tenant's records and assert that none come backBlast radius rises to safety-or-legal and Detectability to never, because a missing filter raises no error
The command handler loads one aggregate, calls one aggregate method that has its own unit tests, and saves, and an endpoint test sends a distinct value in each command field and reads the saved record backDo not unit test the command handler; the endpoint test covers itChange frequency falls to rarely, Detectability to immediately and Reversibility to trivial, because the endpoint test fails in CI when the save is missing or a value lands in the wrong field
Only staff send the command, from an internal back-office toolTest minimally: one main-path test per ruleBlast radius falls to internal and Detectability to same-day, because staff report wrong results that day
HTTP-level tests in CI already send each command with data that tells the outcomes apartDo not add command handler tests; the HTTP-level tests cover the rulesDetectability moves to immediately and Reversibility to trivial, because a broken rule fails CI before merge
A query handler filters read-model rows by status or date rangeTest: one database test per filter, with rows that only the right filter leaves outReversibility stays with-effort, unlike a lookup by ID: code acts on the plausible rows a wrong filter returns

What breaks if you don't test

A reversed date comparison in a command handler books a pickup the carrier cannot serve, the endpoint returns 200, and alerts stay quiet. A customer asks about the parcel days later, and every booking made in between needs repair.

What you lose if you over-test

The common over-test mocks the repository and the event publisher, then verifies that Handle() called each one. Such a test breaks when you rename a repository method and checks no rule. A query handler tested against a mocked DbSet runs its LINQ in memory, which can compare strings case-sensitively where SQL Server does not (EF Core testing guidance).

How to test

Test each rule of a command handler through the mediator: resolve IMediator from the container that WebApplicationFactory builds, replace the repository with an in-memory fake or a test database, send the command, and assert the saved state. A command sent through the mediator also runs every pipeline behavior, so one rejected command per validator proves it is registered. Test query handlers against the production database engine: one test for a lookup by ID, one per filter. Keep one HTTP-level test per endpoint for routing and binding.

When the answer changes

  • The handler moves money or decides which records the caller may see.
  • HTTP-level tests in CI send every command with a distinct value in each field and read the saved record back.

Real incident + Code example

The validator that stopped running

On a logistics API I worked on, our MediatR ValidationBehavior ran each command's registered validators and passed the command on when none were. A refactor moved the shipment validators into a new project, while startup still scanned only the old one with AddValidatorsFromAssemblyContaining<CreateOrderValidator>(). The validator tests built the validator with new, the handler tests called Handle() directly, and both stayed green. One partner's integration sent the order date as the pickup date, so for six days the API accepted past pickups and the carrier's nightly import rejected 140 bookings, which support rebooked by hand. The regression test goes through the real container:

public class ScheduleShipmentPipelineTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly IServiceScope _scope;

    public ScheduleShipmentPipelineTests(WebApplicationFactory<Program> factory)
        => _scope = factory.Services.CreateScope();

    [Fact]
    public async Task RejectsAPickupDateInThePast()
    {
        // Same container and pipeline behaviors as a request from the controller
        var mediator = _scope.ServiceProvider.GetRequiredService<IMediator>();
        var command = new ScheduleShipmentCommand(OrderId: 42, PickupDate: new DateOnly(2026, 1, 5));

        await Assert.ThrowsAsync<ValidationException>(() => mediator.Send(command));
    }
}

If the scan misses the validator again, this test fails.

FAQ

Should I test pure CQRS commands?

No, a command object that only carries data needs no test of its own. Test its validator and its handler by sending the command through the mediator.

Is unit testing the thin read layer necessary?

No, a unit test with a mocked database misses what breaks in a thin read layer: the query and the mapping. Test each query handler against the production database engine instead: one test for a lookup by ID, one test per filter for a handler that filters rows.

Should I test CQRS handlers or the whole request through the Web API?

Test the rules of a CQRS handler through the mediator, and give each endpoint one HTTP-level test for routing, binding and the status code.

Should I mock the repository in a command handler test?

Yes, replace the repository in a command handler test with an in-memory fake that stores real aggregates, and assert the saved state.