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.

ProjectTypeDependenciesTests
TechStrat.Core.TestsUnitNone~164 tests
TechStrat.Api.TestsIntegrationDocker (Testcontainers)~36 tests
TechStrat.Infrastructure.TestsIntegrationDocker (Testcontainers)~37 tests
Terminal window
dotnet test TechStrat.slnx
Terminal window
dotnet test tests/TechStrat.Core.Tests
Terminal window
dotnet test tests/TechStrat.Api.Tests
dotnet test tests/TechStrat.Infrastructure.Tests
  • Framework: xUnit
  • Assertions: FluentAssertions
result.Should().Be(expected);
list.Should().ContainSingle();
act.Should().Throw<InvalidOperationException>();

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

{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()

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

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

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_entity",
TargetType = "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.

  • 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 verify API endpoints using WebApplicationFactory<Program> with a real PostgreSQL database via Testcontainers.

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

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

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
  • 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 verify repository implementations against a real PostgreSQL database using Testcontainers with the postgres:17 image.

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

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.

  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
  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)