Skip to content

Unit Tests ​

Unit tests verify individual services, validators, and event handlers in isolation. They use in-memory SQLite for database-backed tests and fake implementations for cross-module dependencies.

Service Tests ​

Service tests create a real DbContext with an in-memory SQLite connection and test the service directly. Here is the pattern from CustomerServiceTests:

csharp
public sealed class CustomerServiceTests : IDisposable
{
    private readonly CustomersDbContext _db;
    private readonly CustomerService _sut;

    public CustomerServiceTests()
    {
        var options = new DbContextOptionsBuilder<CustomersDbContext>()
            .UseSqlite("Data Source=:memory:")
            .Options;
        var dbOptions = Options.Create(
            new DatabaseOptions
            {
                ModuleConnections = new Dictionary<string, string>
                {
                    ["Customers"] = "Data Source=:memory:",
                },
            }
        );
        _db = new CustomersDbContext(options, dbOptions);
        _db.Database.OpenConnection();
        _db.Database.EnsureCreated();
        _sut = new CustomerService(_db, new TestMessageBus(), NullLogger<CustomerService>.Instance);
    }

    public void Dispose() => _db.Dispose();

    [Fact]
    public async Task CreateCustomerAsync_CreatesAndReturnsCustomer()
    {
        var request = new CreateCustomerRequest { Name = "Alice", Email = "[email protected]" };

        var customer = await _sut.CreateCustomerAsync(request);

        customer.Should().NotBeNull();
        customer.Name.Should().Be("Alice");
        customer.Email.Should().Be("[email protected]");
        customer.Id.Value.Should().BeGreaterThan(0);
    }

    [Fact]
    public async Task UpdateCustomerAsync_WithNonExistentId_ThrowsNotFoundException()
    {
        var request = new UpdateCustomerRequest { Name = "Test", Email = "[email protected]" };

        var act = () => _sut.UpdateCustomerAsync(CustomerId.From(99999), request);

        await act.Should().ThrowAsync<NotFoundException>()
            .WithMessage("*Customer*99999*not found*");
    }
}

Key Pattern

Each test class creates its own in-memory SQLite connection and DbContext. The connection is opened in the constructor and the database schema is created with EnsureCreated(). The IDisposable pattern ensures cleanup.

Validator Tests ​

Validators are pure functions that return a validation result. They are straightforward to test:

csharp
public class CreateRequestValidatorTests
{
    [Fact]
    public void Validate_WithValidRequest_ReturnsSuccess()
    {
        var request = new CreateCustomerRequest { Name = "Alice", Email = "[email protected]" };

        var result = CreateRequestValidator.Validate(request);

        result.IsValid.Should().BeTrue();
    }

    [Fact]
    public void Validate_WithEmptyName_ReturnsError()
    {
        var request = new CreateCustomerRequest { Name = "", Email = "[email protected]" };

        var result = CreateRequestValidator.Validate(request);

        result.IsValid.Should().BeFalse();
        result.Errors.Should().ContainKey("Name");
    }
}

Fake Data Generators ​

The SimpleModule.Tests.Shared project provides pre-built Bogus fakers for all module DTOs and request types in FakeDataGenerators:

csharp
public static class FakeDataGenerators
{
    public static Faker<Customer> CustomerFaker { get; } =
        new Faker<Customer>()
            .RuleFor(c => c.Id, f => CustomerId.From(f.IndexFaker + 1))
            .RuleFor(c => c.Name, f => f.Person.FullName)
            .RuleFor(c => c.Email, f => f.Internet.Email());

    public static Faker<CreateCustomerRequest> CreateCustomerRequestFaker { get; } =
        new Faker<CreateCustomerRequest>()
            .RuleFor(r => r.Name, f => f.Person.FullName)
            .RuleFor(r => r.Email, f => f.Internet.Email());

    public static Faker<User> UserFaker { get; } =
        new Faker<User>()
            .RuleFor(u => u.Id, f => UserId.From(f.IndexFaker + 1))
            .RuleFor(u => u.Email, f => f.Internet.Email())
            .RuleFor(u => u.Name, f => f.Person.FullName)
            .RuleFor(u => u.CreatedAt, f => f.Date.Recent());

    // ... fakers for all module DTOs and request types
}

Use them in tests to generate realistic test data:

csharp
var customers = FakeDataGenerators.CustomerFaker.Generate(5);
var request = FakeDataGenerators.CreateCustomerRequestFaker.Generate();

Fake Contract Implementations ​

For testing code that depends on other modules, the shared project provides fake implementations of contract interfaces. For example, FakeCustomerContracts implements ICustomerContracts with an in-memory list:

csharp
public class FakeCustomerContracts : ICustomerContracts
{
    public List<Customer> Customers { get; set; } =
        FakeDataGenerators.CustomerFaker.Generate(3);

    public Task<IEnumerable<Customer>> GetAllCustomersAsync() =>
        Task.FromResult<IEnumerable<Customer>>(Customers);

    public Task<Customer?> GetCustomerByIdAsync(CustomerId id) =>
        Task.FromResult(Customers.FirstOrDefault(c => c.Id == id));

    public Task<Customer> CreateCustomerAsync(CreateCustomerRequest request)
    {
        var customer = new Customer
        {
            Id = CustomerId.From(_nextId++),
            Name = request.Name,
            Email = request.Email,
        };
        Customers.Add(customer);
        return Task.FromResult(customer);
    }

    // ... other CRUD methods
}

These fakes are useful when a module under test depends on another module's contracts. Rather than spinning up the full dependency, inject the fake:

csharp
var fakeCustomers = new FakeCustomerContracts();
var service = new InvoiceService(fakeCustomers, db, logger);

Testing Event Handlers ​

Wolverine handlers are plain classes — instantiate them directly and call Handle / HandleAsync. To verify a service publishes the right event, substitute IMessageBus and assert on the recorded calls:

csharp
[Fact]
public async Task CreateCustomerAsync_PublishesCustomerCreatedEvent()
{
    var bus = Substitute.For<IMessageBus>();
    var service = new CustomerService(_db, bus, _logger);
    var request = FakeDataGenerators.CreateCustomerRequestFaker.Generate();

    await service.CreateCustomerAsync(request);

    await bus.Received().PublishAsync(Arg.Any<CustomerCreatedEvent>());
}

Next Steps ​

  • Integration Tests -- test HTTP endpoints through the full pipeline
  • E2E Tests -- browser-based testing with Playwright
  • Events -- handler conventions and delivery semantics

Released under the MIT License.