Skip to main content

Testing

RoundTrip uses a multi-layered testing strategy designed to give the team confidence at every level of the stack — from individual domain invariants through to full end-to-end user journeys in the browser. This page describes the strategy, the tools, the current state of the test suite, and what is expected of every developer when writing or maintaining tests.

:::info Current State The test suite exists and runs correctly when triggered manually. Full CI automation — automatic test runs on every PR and deployment — is a known gap that is actively being addressed. Until that is in place, developers are expected to run the relevant test suites manually before merging any PR. See Running Tests Locally below. :::


Testing Philosophy

Tests are not optional. Every ticket closed in Linear must have corresponding tests written in the same session before the ticket is marked Done. Tests written after the fact are tests that will never get written.

The test suite serves three purposes:

  1. Correctness — verifies that the code does what it is supposed to do
  2. Safety net — catches regressions when existing behaviour is changed
  3. Living documentation — domain tests in particular describe business rules in a way that code comments never can

The domain layer — aggregates, value objects, domain services — should have near-100% unit test coverage. This is where the most important business logic lives and where regressions are most costly.


The Test Pyramid

RoundTrip follows the standard test pyramid — many fast unit tests at the base, fewer slower integration tests in the middle, and a small number of E2E tests at the top.


/E\
/ 2 \ Playwright E2E — full browser, real app
/ E \ Slow. Run before releases.
/───────\
/ Integra \ Integration — real SQL Server via Testcontainers
/ tion \ Medium speed. Run before merging.
/─────────────\
/ Application \ Unit — handlers with mocked interfaces
/ Unit Tests \ Fast. Run on every change.
/───────────────────\
/ Domain Unit \ Unit — aggregates, value objects, domain services
\ Tests / Fastest. No dependencies. Run constantly.
\───────────────────/
Test TypeProjectSpeedWhen to Run
Domain UnitRoundTrip.Domain.TestsInstantEvery change
Application UnitRoundTrip.Application.TestsFastEvery change
IntegrationRoundTrip.Integration.TestsMediumBefore merging a PR
ArchitectureRoundTrip.Architecture.TestsFastBefore merging a PR
E2E (Playwright)RoundTrip.E2E.TestsSlowBefore releases and manually

Test Types

Domain Unit Tests

Domain tests are the most important tests in the codebase. They test aggregate invariants, value object validation, state machine transitions, and domain event raising — with zero external dependencies. No database, no mocks, no HTTP.

Tools: xUnit, FluentAssertions

[Fact]
public void Complete_WhenInProgress_SetsStatusAndRaisesEvent()
{
// Arrange
var ticket = ServiceTicketBuilder.Create()
.WithStatus(TicketStatus.InProgress)
.Build();

// Act
ticket.Complete("Replaced the compressor unit, tested OK");

// Assert
ticket.Status.Should().Be(TicketStatus.Completed);
ticket.CompletedAt.Should().NotBeNull();
ticket.DomainEvents.Should().ContainSingle()
.Which.Should().BeOfType<TicketCompletedEvent>();
}

[Fact]
public void Complete_WhenNotInProgress_ThrowsDomainException()
{
var ticket = ServiceTicketBuilder.Create()
.WithStatus(TicketStatus.Assigned)
.Build();

var act = () => ticket.Complete("Some note");

act.Should().Throw<DomainException>()
.WithMessage("*Can only complete an InProgress ticket*");
}

What to test in domain tests:

  • Every valid state transition on every aggregate
  • Every invariant guard — tests that the domain throws DomainException when rules are violated
  • Value object equality, creation rules, and validation
  • That domain events are raised at the right moment with the right data

Application Unit Tests

Application tests verify handler logic, validator rules, and pipeline behavior interactions. Infrastructure interfaces are mocked using NSubstitute.

Tools: xUnit, FluentAssertions, NSubstitute

[Fact]
public async Task Handle_WhenTicketNotFound_ReturnsNotFound()
{
// Arrange
var tickets = Substitute.For<IServiceTicketRepository>();
tickets.GetByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
.Returns((ServiceTicket?)null);

var handler = new CompleteTicketHandler(tickets, Substitute.For<IUnitOfWork>());

// Act
var result = await handler.Handle(
new CompleteTicketCommand(Guid.NewGuid(), "Some note"),
CancellationToken.None);

// Assert
result.IsSuccess.Should().BeFalse();
result.ResultType.Should().Be(ResultType.NotFound);
}

Integration Tests

Integration tests verify the full command/query flow against a real SQL Server instance spun up via Testcontainers. They test EF Core migrations, tenant isolation, Dapper queries, and cross-aggregate event dispatch.

Tools: xUnit, Testcontainers, FluentAssertions

:::caution Testcontainers Requires Docker Integration tests require Docker to be running locally. The test base class spins up a real SQL Server container automatically for each test run and tears it down afterwards. :::

What integration tests must verify:

  • Commands produce the expected database state
  • Queries return the correct data for the requesting tenant
  • Cross-tenant isolation — tenant A cannot see tenant B's data under any circumstances
  • Domain events dispatch correctly and produce expected side effects

Architecture Tests

Architecture tests use NetArchTest to enforce layer boundary rules automatically. These tests fail the build if any layer dependency rule is violated — preventing architectural drift before it becomes a problem.

Tools: NetArchTest, xUnit, FluentAssertions

See Clean Architecture for the full set of architecture tests.

End-to-End Tests (Playwright)

Playwright tests exercise the full application — frontend to backend — in a real browser. They cover critical user journeys such as logging in, creating a ticket, assigning it, completing it, and viewing the invoice.

Tools: Playwright, TypeScript

See Playwright Testing for setup, conventions, and how to write and run E2E tests.

:::warning Playwright Tests May Be Out of Date The Playwright test suite was written against an earlier version of the application and may not reflect the current UI. Before relying on Playwright tests for a release, verify that the tests pass against the current build and update any that have drifted. Keeping E2E tests current is a shared team responsibility. :::


Running Tests Locally

Until CI automation is fully in place, run these commands manually before merging any PR.

All unit and architecture tests

# From the repo root
dotnet test tests/RoundTrip.Domain.Tests
dotnet test tests/RoundTrip.Application.Tests
dotnet test tests/RoundTrip.Architecture.Tests

Or run all at once:

dotnet test --filter "Category!=Integration"

Integration tests (requires Docker)

# Ensure Docker is running first
dotnet test tests/RoundTrip.Integration.Tests

Playwright E2E tests

# From the web repo root
npx playwright test

# Run with UI for debugging
npx playwright test --ui

# Run a specific test file
npx playwright test tests/ticket-lifecycle.spec.ts

See Playwright Testing for full setup instructions.


What Every Developer Must Do

These are non-negotiable expectations for every ticket:

  • Write domain unit tests for any new aggregate behaviour or value object
  • Write application unit tests for any new handler
  • Write or update integration tests for any change that touches the database schema or query logic
  • Run dotnet test --filter "Category!=Integration" before opening a PR
  • Run integration tests before merging if your change touches EF Core, Dapper queries, or tenant scoping
  • Update Playwright tests if your change modifies a UI flow covered by existing E2E tests
  • Never mark a Linear ticket as Done without tests written

Known Gaps & Roadmap

These are known deficiencies in the current test setup that the team is working to address:

GapPriorityNotes
Unit and integration tests not running automatically in CIHighADO pipeline needs test steps added to both dev and prod pipelines
Playwright tests not running in CIHighRequires a pipeline agent with a browser environment configured
Playwright tests may have drifted from current UIMediumManual audit needed — update tests that no longer reflect the app
Integration test coverage may have gaps from fast-moving feature workMediumCoverage audit recommended before first paid customer onboards

Tools Reference

ToolVersionPurpose
xUnitLatestTest runner for all .NET tests
FluentAssertionsLatestReadable assertion syntax
NSubstituteLatestMocking library for application unit tests
TestcontainersLatestSpins up real SQL Server for integration tests
NetArchTestLatestArchitecture boundary enforcement
PlaywrightLatestBrowser automation for E2E tests
BogusLatestFake data generation for test builders

Something broken in the test suite? Add a note to the Notes section so the team knows about it.