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 ProductServiceTests:

csharp
public sealed class ProductServiceTests : IDisposable
{
    private readonly ProductsDbContext _db;
    private readonly ProductService _sut;

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

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

    [Fact]
    public async Task CreateProductAsync_CreatesAndReturnsProduct()
    {
        var request = new CreateProductRequest { Name = "Test Widget", Price = 19.99m };

        var product = await _sut.CreateProductAsync(request);

        product.Should().NotBeNull();
        product.Name.Should().Be("Test Widget");
        product.Price.Should().Be(19.99m);
        product.Id.Value.Should().BeGreaterThan(0);
    }

    [Fact]
    public async Task UpdateProductAsync_WithNonExistentId_ThrowsNotFoundException()
    {
        var request = new UpdateProductRequest { Name = "Test", Price = 10.00m };

        var act = () => _sut.UpdateProductAsync(ProductId.From(99999), request);

        await act.Should().ThrowAsync<NotFoundException>()
            .WithMessage("*Product*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 CreateProductRequest { Name = "Widget", Price = 9.99m };

        var result = CreateRequestValidator.Validate(request);

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

    [Fact]
    public void Validate_WithEmptyName_ReturnsError()
    {
        var request = new CreateProductRequest { Name = "", Price = 9.99m };

        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<Product> ProductFaker { get; } =
        new Faker<Product>()
            .RuleFor(p => p.Id, f => ProductId.From(f.IndexFaker + 1))
            .RuleFor(p => p.Name, f => f.Commerce.ProductName())
            .RuleFor(p => p.Price, f => f.Finance.Amount(1, 1000));

    public static Faker<CreateProductRequest> CreateProductRequestFaker { get; } =
        new Faker<CreateProductRequest>()
            .RuleFor(r => r.Name, f => f.Commerce.ProductName())
            .RuleFor(r => r.Price, f => f.Finance.Amount(1, 1000));

    public static Faker<Order> OrderFaker { get; } =
        new Faker<Order>()
            .RuleFor(o => o.Id, f => OrderId.From(f.IndexFaker + 1))
            .RuleFor(o => o.UserId, f => UserId.From(
                f.Random.Int(1, 100).ToString(CultureInfo.InvariantCulture)))
            .RuleFor(o => o.Items, f => OrderItemFaker.Generate(f.Random.Int(1, 3)))
            .RuleFor(o => o.Total, f => f.Finance.Amount(10, 500))
            .RuleFor(o => o.CreatedAt, f => f.Date.Recent());

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

Use them in tests to generate realistic test data:

csharp
var products = FakeDataGenerators.ProductFaker.Generate(5);
var request = FakeDataGenerators.CreateProductRequestFaker.Generate();

Fake Contract Implementations ​

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

csharp
public class FakeProductContracts : IProductContracts
{
    public List<Product> Products { get; set; } =
        FakeDataGenerators.ProductFaker.Generate(3);

    public Task<IEnumerable<Product>> GetAllProductsAsync() =>
        Task.FromResult<IEnumerable<Product>>(Products);

    public Task<Product?> GetProductByIdAsync(ProductId id) =>
        Task.FromResult(Products.FirstOrDefault(p => p.Id == id));

    public Task<Product> CreateProductAsync(CreateProductRequest request)
    {
        var product = new Product
        {
            Id = ProductId.From(_nextId++),
            Name = request.Name,
            Price = request.Price,
        };
        Products.Add(product);
        return Task.FromResult(product);
    }

    // ... 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 fakeProducts = new FakeProductContracts();
var service = new OrderService(fakeProducts, db, logger);

Testing Event Handlers ​

Event handlers are tested by invoking HandleAsync directly and asserting on side effects:

csharp
[Fact]
public async Task HandleAsync_LogsEvent()
{
    var handler = new AuditLogEventHandler(logger);
    var @event = new OrderCreatedEvent { OrderId = OrderId.From(1) };

    await handler.HandleAsync(@event, CancellationToken.None);

    // Assert on side effects (e.g., logger calls, database writes)
}

For testing the EventBus itself, including partial failure semantics, see the EventBusPartialFailureTests in the core test project.

Next Steps ​

  • Integration Tests -- test HTTP endpoints through the full pipeline
  • E2E Tests -- browser-based testing with Playwright
  • Event Bus -- understand event handler patterns and partial failure

Released under the MIT License.