# Testing a feature

> Unit-test handlers and aggregates with the fast suite, and cover endpoints with Testcontainers integration tests.

## Three test projects, two speeds

A vertical slice is tested at the level its risk lives. Pure logic, an aggregate
invariant, an error mapping, a validator, runs as a millisecond unit test. Anything that
touches the real schema, ASP.NET Identity or the domain-event flow runs against a real
Postgres in a container. The split maps onto three projects under `api/tests/`:

```
api/tests/
  Slicekit.Unit.Tests/          # pure logic: aggregates, handlers with mocked ports, validators
  Slicekit.Architecture.Tests/  # NetArchTest rules: slices stay isolated, layers stay clean
  Slicekit.Feature.Tests/       # integration: real Postgres via Testcontainers, no HTTP
```

The first two are the **fast suite**. They need no Docker and finish in a few seconds, so
they are the loop you run on every change:

```sh
dotnet test api/tests/Slicekit.Unit.Tests api/tests/Slicekit.Architecture.Tests --nologo
```

The third, `Slicekit.Feature.Tests`, is slower (Testcontainers spins up a Postgres per
fixture) and is the boundary where you prove the slice works against the real database.

This page walks all three: a unit test for a handler and an aggregate, the architecture
tests that keep slices honest, and an integration test backed by Testcontainers. For the
shape of the slice itself, see [Adding a vertical slice](/docs/vertical-slices).

## 1. Unit-test the aggregate

The aggregate owns its invariants, so test it in isolation with no infrastructure. These
live in `Slicekit.Unit.Tests/Domain/`. Construct the aggregate, call a method, assert on the
resulting state:

```csharp
using Slicekit.Core.Domain;

namespace Slicekit.Unit.Tests.Domain;

public sealed class UserTests
{
    [Fact]
    public void CreateLocal_Sets_Defaults_For_New_Local_Account()
    {
        var user = User.CreateLocal("user@example.com");

        user.Id.ShouldNotBe(Guid.Empty);
        user.Email.ShouldBe("user@example.com");
        user.Enabled.ShouldBeTrue();
        user.IsAdmin.ShouldBeFalse();
    }

    [Fact]
    public void AssignPermission_Is_Idempotent_And_Returns_False()
    {
        var user = User.CreateLocal("user@example.com");
        var permission = new Permission { Id = 7, Name = "Test.Permission" };
        user.AssignPermission(user.Id, permission);

        var added = user.AssignPermission(user.Id, permission);

        added.ShouldBeFalse();
        user.UserPermissions.ShouldHaveSingleItem();
    }
}
```

Assertions use [Shouldly](https://docs.shouldly.org/). `Globals.cs` in the project
glob-imports `Shouldly`, `Xunit`, `Slicekit.Core.Common` and the error namespaces, so those
types are unqualified inside test files.

## 2. Unit-test the handler

A handler that does not touch the database can also be a unit test: inject its ports as
NSubstitute mocks and assert on what it dispatched. These live next to the slice they
cover, for example `Slicekit.Unit.Tests/Auth/`:

```csharp
using NSubstitute;
using Slicekit.Core.Domain.Events;
using Slicekit.Core.Features.Auth.RevokeAllRefreshTokens;
using Wolverine;

namespace Slicekit.Unit.Tests.Auth;

public sealed class RevokeSessionsOnUserDisabledHandlerTests
{
    [Fact]
    public async Task Publishes_Revoke_Command_When_User_Disabled()
    {
        var bus = Substitute.For();
        var userId = Guid.NewGuid();
        var handler = new RevokeSessionsOnUserDisabledHandler(bus);

        await handler.Handle(new UserEnabledChangedEvent(Guid.NewGuid(), userId, Enabled: false));

        await bus.Received(1).PublishAsync(
            Arg.Is(c => c.UserId == userId),
            Arg.Any<DeliveryOptions?>());
    }
}
```

The rule of thumb: if you can mock the handler's ports and still test the behaviour you
care about, keep it here. The moment the assertion is about EF behaviour, the schema, or
cross-aggregate state, promote it to an integration test (section 4).

## 3. Let the architecture tests guard the slice

`Slicekit.Architecture.Tests` uses [NetArchTest](https://github.com/BenMorris/NetArchTest)
to enforce the boundaries that make vertical slices work. You do not write a new test per
feature: the existing rules scan every type in `Slicekit.Core` and fail if your slice breaks
one. The two that bite most often:

```csharp
[Fact]
public void Feature_Slices_Must_Not_Depend_On_Each_Other()
{
    foreach (var ns in FeatureNamespaces)
    {
        var otherFeatures = FeatureNamespaces.Where(f => f != ns).ToArray();
        var result = Types.InAssembly(CoreAssembly)
            .That().ResideInNamespace(ns)
            .ShouldNot().HaveDependencyOnAny(otherFeatures)
            .GetResult();

        result.IsSuccessful.ShouldBeTrue();
    }
}
```

If your new slice references a type from another slice, this fails and names the offending
type. The fix is to share through `Slicekit.Core.Domain`, not across feature folders. A
companion rule, `Domain_Must_Not_Depend_On` (a theory with one case per forbidden
dependency), keeps the domain model free of EF Core, Identity, Wolverine and HTTP. Run the fast suite and
these pass or point straight at the line to fix.

## 4. Integration-test the endpoint with Testcontainers

When the slice touches the database, identity or the domain-event flow, write an
integration test in `Slicekit.Feature.Tests/Features//`. These instantiate the handler
directly against a real Postgres (Testcontainers), call `HandleAsync`, and assert on the
`Result`, the queued `OutgoingMessages`, and any domain events the aggregate raised. No
HTTP, no mocking of EF.

### The fixture

`DatabaseFixture` spins up one Postgres container per xUnit collection, applies every
migration, and seeds the permission catalog. Tests share the container; each test gets a
fresh `AppDbContext` and truncates the user-data tables on setup. Inherit
`FeatureTestBase` and you get a clean `Db` per test:

```csharp
[Collection("Database")]
public abstract class FeatureTestBase(DatabaseFixture db) : IAsyncLifetime
{
    protected AppDbContext Db { get; private set; } = null!;

    public async Task InitializeAsync()
    {
        Db = db.CreateDbContext();
        await db.TruncateUserDataAsync();
    }

    public async Task DisposeAsync() => await Db.DisposeAsync();
}
```

The `[Collection("Database")]` attribute is required and is inherited from the base class.
Without it xUnit treats the class as parallel-isolated and spins up its own container.

### The test

Arrange state with the seeding helpers, call the handler, assert on all three outputs:

```csharp
public sealed class CreateProjectTests(DatabaseFixture db) : FeatureTestBase(db)
{
    [Fact]
    public async Task Happy_Path()
    {
        // Arrange: seed, save, clear the change tracker
        var user = User.CreateLocal("u@example.com");
        Db.Users.Add(user);
        await Db.SaveChangesAsync();
        Db.ChangeTracker.Clear();

        // Act: instantiate the handler, call HandleAsync
        var handler = new CreateProjectCommandHandler(Db);
        var (result, messages) = await handler.HandleAsync(new CreateProjectCommand(user.Id, "First"));

        // Assert: Result, OutgoingMessages, domain events
        result.IsSuccess.ShouldBeTrue();
        Db.DomainEvents().OfType().ShouldHaveSingleItem();
    }
}
```

`IdentityHelper` builds the ASP.NET Identity plumbing that registering users needs:

```csharp
var userManager = IdentityHelper.BuildUserManager(Db);
var (appUser, domainUser) = await IdentityHelper.SeedUserAsync(Db, userManager, "u@example.com", "Password1!");
```

`SeedUserAsync` creates both the `ApplicationUser` and the `User` aggregate and links them
by `Id`. Pass `password: null` for a passwordless (OAuth-only) account. For a domain-only
seed with no Identity row, use `IdentityHelper.SeedDomainUser(Db, "u@example.com")`.

### Domain events

The fixture wires a capturing interceptor into every `AppDbContext`, snapshotting
`IDomainEvent`s on `SaveChanges` at the same point Wolverine's transactional middleware
uses in production. Read them back with the extension:

```csharp
var events = Db.DomainEvents();
events.OfType().ShouldHaveSingleItem();
```

`DomainEvents()` is cumulative per `Db` instance; call `Db.ClearDomainEvents()` for a
clean slate between multiple `SaveChanges` calls in one test.

### Outgoing messages

Command handlers return `(Result, OutgoingMessages)`. The `OutgoingMessages` are the
messages the handler would dispatch via Wolverine. Assert on them directly; the dispatch
itself does not run, which is the right boundary because the message contract is what your
slice owns and the recipient's behaviour is its own test:

```csharp
var (result, messages) = await handler.HandleAsync(command);

result.IsSuccess.ShouldBeTrue();
var email = messages.OfType().ShouldHaveSingleItem();
email.Recipient.ShouldBe("u@example.com");
```

### Mocking ports, not EF

External dependencies (`IAuditService`, breached-password checks, OAuth providers) arrive
as interfaces on the handler constructor. Mock those with NSubstitute:

```csharp
var audit = Substitute.For();
var handler = new UpdateConsentCommandHandler(Db, audit);

await handler.HandleAsync(command);

await audit.Received(1).EmitAsync(
    Arg.Is(e => e.Category == AuditCategory.Consent && e.Action == "Consent.Granted"),
    Arg.Any());
```

Do not mock `AppDbContext`, and do not mock `UserManager`. Using the real Postgres through
the fixture and the helpers is the whole point of an integration test.

## Running the suites

```sh
# Fast loop: unit + architecture, no Docker, a few seconds
dotnet test api/tests/Slicekit.Unit.Tests api/tests/Slicekit.Architecture.Tests --nologo

# Integration: needs Docker running for Testcontainers
dotnet test api/tests/Slicekit.Feature.Tests --nologo
```

If Postgres will not start, `docker compose ps` confirms Docker is up. The Testcontainers
Postgres is its own container, separate from the one in `docker-compose.yml`.

## Checklist

- Aggregate invariants and pure logic covered by unit tests in `Slicekit.Unit.Tests`.
- Handlers with mockable ports unit-tested with NSubstitute; no EF or `UserManager` mocks.
- Fast suite green: `dotnet test api/tests/Slicekit.Unit.Tests api/tests/Slicekit.Architecture.Tests --nologo`.
- Architecture tests pass, so the new slice does not reach across feature boundaries.
- Database, identity or domain-event behaviour covered by an integration test in
  `Slicekit.Feature.Tests`, inheriting `FeatureTestBase` with `[Collection("Database")]`.
- Asserted on all three outputs that matter: the `Result`, the `OutgoingMessages`, and the
  raised domain events.
- `dotnet test api/tests/Slicekit.Feature.Tests --nologo` passes with Docker running.
