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:
- Correctness — verifies that the code does what it is supposed to do
- Safety net — catches regressions when existing behaviour is changed
- 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 Type | Project | Speed | When to Run |
|---|---|---|---|
| Domain Unit | RoundTrip.Domain.Tests | Instant | Every change |
| Application Unit | RoundTrip.Application.Tests | Fast | Every change |
| Integration | RoundTrip.Integration.Tests | Medium | Before merging a PR |
| Architecture | RoundTrip.Architecture.Tests | Fast | Before merging a PR |
| E2E (Playwright) | RoundTrip.E2E.Tests | Slow | Before 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
DomainExceptionwhen 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:
| Gap | Priority | Notes |
|---|---|---|
| Unit and integration tests not running automatically in CI | High | ADO pipeline needs test steps added to both dev and prod pipelines |
| Playwright tests not running in CI | High | Requires a pipeline agent with a browser environment configured |
| Playwright tests may have drifted from current UI | Medium | Manual audit needed — update tests that no longer reflect the app |
| Integration test coverage may have gaps from fast-moving feature work | Medium | Coverage audit recommended before first paid customer onboards |
Tools Reference
| Tool | Version | Purpose |
|---|---|---|
| xUnit | Latest | Test runner for all .NET tests |
| FluentAssertions | Latest | Readable assertion syntax |
| NSubstitute | Latest | Mocking library for application unit tests |
| Testcontainers | Latest | Spins up real SQL Server for integration tests |
| NetArchTest | Latest | Architecture boundary enforcement |
| Playwright | Latest | Browser automation for E2E tests |
| Bogus | Latest | Fake data generation for test builders |
Something broken in the test suite? Add a note to the Notes section so the team knows about it.