Skip to content

Testing

The Tech Strategy Tool has three test projects covering domain logic, API behavior, and data persistence. This page documents the testing conventions, patterns, and how to run tests.

Test Projects

Project Type Dependencies Tests
TechStrat.Core.Tests Unit None ~164 tests
TechStrat.Api.Tests Integration Docker (Testcontainers) ~36 tests
TechStrat.Infrastructure.Tests Integration Docker (Testcontainers) ~37 tests

Running Tests

All tests

dotnet test TechStrat.slnx

Docker required

Integration tests use Testcontainers to spin up PostgreSQL instances. Docker must be running.

Unit tests only (no Docker)

dotnet test tests/TechStrat.Core.Tests

Specific test project

dotnet test tests/TechStrat.Api.Tests
dotnet test tests/TechStrat.Infrastructure.Tests

Conventions

Framework and assertions

  • Framework: xUnit
  • Assertions: FluentAssertions
result.Should().Be(expected);
list.Should().ContainSingle();
act.Should().Throw<InvalidOperationException>();

Test class naming

{Feature}Tests — e.g., EventProcessorCreateTests, EventStoreTests

Test method naming

{Scenario}_description using descriptive snake_case-style naming:

[Fact]
public void Creating_a_team_adds_it_to_the_document()

[Fact]
public void Deleting_a_principle_removes_it_from_all_objectives()

[Fact]
public void Updating_with_same_value_returns_no_change()

Independence

No test should depend on another test's side effects. Each test creates its own state from scratch.

Unit Tests (Core.Tests)

Unit tests cover the event processor's domain logic. They are fast, require no infrastructure, and form the bulk of the test suite.

Test data builders

Use helper methods or builder classes that construct a processor with known state by feeding a sequence of events:

private static EventProcessor CreateProcessorWithTeam(out Guid teamId)
{
    var processor = new EventProcessor();
    teamId = Guid.NewGuid();
    processor.ProcessEvent(new EventEnvelope
    {
        EventType = "create_team",
        ActorId = Guid.NewGuid(),
        Data = new Dictionary<string, string>
        {
            ["id"] = teamId.ToString(),
            ["name"] = "Test Team",
            ["color"] = "#3498db"
        }
    });
    return processor;
}

This pattern avoids duplicated setup boilerplate across test classes.

What to test

  • Event processing: correct state changes, proper rejections
  • Validation: required fields, field limits, entity existence
  • Conflict detection: lastSeenSequence handling
  • No-op detection: same-value updates
  • Cascade behavior: delete cascades, reference cleanup
  • Event decomposition: derived commands producing correct events
  • Notifications: correct notification types and entity IDs

Integration Tests (Api.Tests)

Integration tests verify API endpoints using WebApplicationFactory<Program> with a real PostgreSQL database via Testcontainers.

Test infrastructure

  • TestWebApplicationFactory — configures the test host with a Testcontainers PostgreSQL instance
  • TestHelper — provides typed helpers for common operations

Authentication in tests

HTTP-only Secure cookies do not round-trip in WebApplicationFactory's internal pipeline. Instead, seed users and sessions directly in the database:

var client = await TestHelper.SeedUserAndLogin(factory, "editor", "editor", "editor");
// client is now an authenticated HttpClient

Helper methods

TestHelper provides typed helpers that handle event submission with proper structure:

  • CreateTeam(client, name, color) — creates a team and returns the ID
  • CreatePrinciple(client, teamId, title) — creates a principle
  • CreateObjective(client, teamId, title) — creates an objective
  • SubmitEvent(client, eventType, targetId, data) — submits any event

Create events require data[\"id\"]

All create event helpers include data["id"] with the new entity's GUID. This is not the TargetId (which is the parent entity). Missing this field causes a rejection.

What to test

  • Endpoint routing and HTTP status codes
  • Authorization policies (correct role requirements)
  • Request/response serialization
  • Event processing through the full pipeline
  • CSRF header enforcement
  • Error responses

Persistence Tests (Infrastructure.Tests)

Persistence tests verify repository implementations against a real PostgreSQL database using Testcontainers with the postgres:17 image.

What to test

  • CRUD operations on all repositories
  • Query behavior (filtering, ordering, pagination)
  • Index effectiveness
  • JSONB serialization/deserialization
  • Cascade deletes
  • Unique constraint enforcement

Package References

Test project .csproj files use <PackageReference Include="..." /> without versions:

<PackageReference Include="xunit" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />

Versions are managed centrally in Directory.Packages.props.

Tips

Debugging test failures

  1. Check Docker — integration test failures often mean Docker is not running
  2. Run unit tests first — they are fast and catch domain logic issues without infrastructure noise
  3. Read assertion messages — FluentAssertions provides descriptive failure messages
  4. Check event rejections — if a state setup step fails, subsequent assertions will also fail. Check ProcessingResult.Status and RejectionReason

Writing new tests

  1. Identify the closest existing test class for your feature
  2. Follow the naming convention: {Scenario}_description
  3. Use builder methods to set up state
  4. Assert specific outcomes, not implementation details
  5. Keep tests focused — one logical assertion per test (FluentAssertions chains count as one)