Files
the-biergarten-app/docs/website/testing.md
2026-08-31 14:16:33 -04:00

12 KiB

Testing

This document describes the testing strategy and how to run tests for The Biergarten App.

Overview

The project uses a multi-layered testing approach across backend and frontend:

  • API.Specs - BDD integration tests using Reqnroll (Gherkin), run against a live, seeded database
  • Features.*.Tests - One unit test project per backend feature slice (Features.Users.Tests, Features.Breweries.Tests, Features.Emails.Tests, Features.PhotoUpload.Tests), covering that slice's command/query handlers, with dependencies mocked via Moq (no real database required)
  • Storybook Vitest project - Browser-based interaction tests for shared website stories
  • Storybook Playwright suite - Browser checks against Storybook-rendered components

The easiest way to run all tests is using Docker Compose, which sets up an isolated test environment:

docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abort-on-container-exit

This command:

  1. Starts a fresh SQL Server instance
  2. Runs database migrations
  3. Seeds test data
  4. Executes all test suites in parallel, including the frontend's Storybook Vitest and Playwright suites (frontend.tests, which needs no database)
  5. Exports results to ./test-results/
  6. Exits when tests complete

View test results

# List test result files
ls -la test-results/

# View specific test results
cat test-results/api-specs/results.trx
cat test-results/Features.Users.Tests.trx
cat test-results/Features.Breweries.Tests.trx
cat test-results/Features.Emails.Tests.trx
cat test-results/Features.PhotoUpload.Tests.trx
cat test-results/frontend/vitest-storybook.xml
cat test-results/frontend/playwright-storybook.xml

Clean up

# Remove test containers and volumes
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v

Running tests locally

You can run individual test projects locally without Docker:

Integration tests (API.Specs)

cd web/backend
dotnet test API/API.Specs/API.Specs.csproj

Requirements:

  • SQL Server instance running
  • Database migrated and seeded
  • Environment variables set (DB connection, JWT secret)

Feature slice unit tests

Each feature slice has its own test project, covering its command/query handlers:

cd web/backend
dotnet test Features/Features.Users.Tests/Features.Users.Tests.csproj
dotnet test Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj
dotnet test Features/Features.Emails.Tests/Features.Emails.Tests.csproj
dotnet test Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj

# Or run all of them at once via the solution:
for proj in Features/*.Tests; do dotnet test "$proj"; done

Requirements:

  • No database required (handlers are tested with their repository/service dependencies mocked via Moq)

Frontend Storybook tests

cd web/frontend
npm install
npm run test:storybook

Purpose:

  • Verifies shared stories such as form fields, submit buttons, navbar states, toasts, and the theme gallery
  • Runs in browser mode via Vitest and Storybook integration

Frontend Playwright Storybook tests

cd web/frontend
npm install
npm run test:storybook:playwright

Requirements:

  • Storybook dependencies installed
  • Playwright browser dependencies installed
  • The command will start or reuse the Storybook server defined in playwright.storybook.config.ts

Running Storybook tests with a visible browser

Both of the above run headless by default. To watch the browser while debugging a failure:

# Playwright Storybook suite
npx playwright test -c playwright.storybook.config.ts --headed
# or the interactive step-through runner
npx playwright test -c playwright.storybook.config.ts --ui

# Vitest/story play-function suite
npx vitest run --project storybook --browser.headless=false
# or Vitest's own UI (drop `run` for watch mode)
npx vitest --project storybook --browser.headless=false --ui

The default (headless: true) is set in web/frontend/vite.config.ts.

Test coverage

Current coverage

Features.Users.Tests:

  • User registration with validation
  • User login with JWT token generation
  • Password hashing and verification (Argon2id)
  • Email confirmation and confirmation-email resend
  • Refresh token exchange
  • JWT token generation, validation, and claims handling
  • Invalid credentials and 404 error responses (via API.Specs)
  • Account management: username, email, password, and profile updates, and account deletion (blocked while dependent posts, comments, photos, or follows exist)
  • User get-by-id/get-all queries

Features.Breweries.Tests:

  • Brewery create/update/delete commands and get-by-id/get-all queries
  • Brewery location queries: all locations, and locations within a given range of a coordinate

Features.Emails.Tests:

  • Registration and resend-confirmation email dispatch handlers

Features.PhotoUpload.Tests:

  • Upload command handler: storage key construction, content type passthrough, and photo persistence via the mocked storage provider and repository
  • Upload validator: min/max file size bounds, and PNG/JPEG/WebP signature checks (including rejection of files with a mismatched or missing signature)

Frontend UI Coverage:

  • Shared submit button states
  • Form field happy path and error presentation
  • Navbar guest, authenticated, and mobile behavior
  • Theme gallery rendering across Biergarten themes
  • Toast interactions and themed notification display

Planned coverage

  • Password reset functionality
  • Beer post operations
  • User follow/unfollow
  • Frontend route integration coverage beyond Storybook stories

Testing frameworks and tools

xUnit

  • Primary unit testing framework
  • Used for handler and service layer tests
  • Supports parallel test execution

Reqnroll (Gherkin/BDD)

  • Behavior-driven development framework
  • Used for API integration tests
  • Human-readable test scenarios in .feature files

FluentAssertions

  • Expressive assertion library
  • Makes test assertions more readable
  • Used across all test projects

Moq

  • Mocking framework for .NET
  • Used in Service layer tests
  • Enables isolated unit testing

Test structure

API.Specs (integration tests)

API.Specs/
├── Features/
│   ├── Registration.feature            # Registration scenarios
│   ├── Login.feature                   # Login scenarios
│   ├── Confirmation.feature            # Email confirmation scenarios
│   ├── ResendConfirmation.feature      # Resend-confirmation scenarios
│   ├── TokenRefresh.feature            # Refresh token scenarios
│   ├── AccessTokenValidation.feature   # Protected endpoint access scenarios
│   └── NotFound.feature                # 404 handling
├── Steps/
│   ├── AuthSteps.cs                    # Step definitions for the Auth features
│   └── ApiGeneralSteps.cs              # Shared/general step definitions
├── Mocks/
│   ├── MockEmailDispatcher.cs          # Substitutes Features.Emails' IEmailDispatcher
│   └── MockEmailProvider.cs            # Substitutes Infrastructure.Email's IEmailProvider
└── TestApiFactory.cs                   # Test server setup (swaps in the mocks above)

Example Feature:

Feature: User Registration
  As a user
  I want to register
  So that I can access the platform

Scenario: Successful user registration
  Given I have valid registration details
  When I register a new account
  Then I should receive a JWT token
  And my account should be created

Features.Users.Tests

Features.Users.Tests/
├── Commands/
│   ├── RegisterUserHandlerTests.cs
│   ├── ConfirmUserHandlerTests.cs
│   ├── ResendConfirmationEmailHandlerTests.cs
│   ├── RefreshTokenHandlerTests.cs
│   ├── UpdateUsernameHandlerTests.cs
│   ├── UpdateEmailHandlerTests.cs
│   ├── UpdatePasswordHandlerTests.cs
│   ├── UpdateProfileHandlerTests.cs
│   └── DeleteAccountHandlerTests.cs
├── Queries/
│   ├── LoginHandlerTests.cs            # tests LoginCommand, despite the folder name
│   ├── GetAllUsersHandlerTests.cs
│   └── GetUserByIdHandlerTests.cs
├── Services/
│   ├── TokenServiceRefreshTests.cs
│   └── TokenServiceValidationTests.cs
└── TestSupport/                        # shared test fixtures/helpers

Each of the other slices (Features.Breweries.Tests, Features.Emails.Tests, Features.PhotoUpload.Tests) follows the same shape: a Commands//Queries/ folder with one test file per handler (Features.PhotoUpload.Tests also adds a validator test file alongside its handler test).

Writing tests

Unit test example (xUnit)

public class LoginHandlerTests
{
    [Fact]
    public async Task Handle_WithValidData_ReturnsPayloadWithMatchingUsername()
    {
        // Arrange
        var authRepoMock = new Mock<IAuthRepository>();
        var passwordInfraMock = new Mock<IPasswordInfrastructure>();
        var tokenServiceMock = new Mock<ITokenService>();
        var handler = new LoginHandler(authRepoMock.Object, passwordInfraMock.Object, tokenServiceMock.Object);
        // ...set up mocks for a known user/credential...

        // Act
        var result = await handler.Handle(new LoginCommand("testuser", "password123"), CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result.AccessToken.Should().NotBeNullOrEmpty();
    }
}

Integration test example (Reqnroll)

Scenario: User login with valid credentials
  Given a registered user with username "testuser"
  When I POST to "/api/auth/login" with valid credentials
  Then the response status should be 200
  And the response should contain a JWT token

Continuous integration

The .github/workflows/tests.yml GitHub Actions workflow runs on every push and pull request to main. It runs the same test Docker Compose configuration used locally, so backend and frontend tests execute in the same containerized environment in CI as they do on a developer machine:

# CI/CD command
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml build
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abort-on-container-exit
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v

Because --abort-on-container-exit stops the stack as soon as any one-shot test container exits (success or failure), the workflow separately inspects the exit code of each test container (test-env-api-specs, test-env-unit-tests, test-env-frontend-tests) after up returns and fails the job if any of them is non-zero. ./test-results/ is uploaded as a build artifact regardless of outcome.

Exit codes:

  • 0 - All tests passed
  • Non-zero - Test failures occurred

The frontend's Storybook Vitest and Playwright suites run inside the frontend.tests container (see Docker Guide) as part of that same compose run, so no separate frontend CI step is needed. To run them locally without Docker:

cd web/frontend
npm ci
npm run test:storybook
npm run test:storybook:playwright

Troubleshooting

Tests failing due to database connection

Ensure SQL Server is running and environment variables are set:

docker compose --env-file web/.env.test -f web/docker-compose.test.yaml ps

Port conflicts

If port 1433 is in use, stop other SQL Server instances or modify the port in docker-compose.test.yaml.

Stale test data

Clean up test database:

docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v

View container logs

docker compose --env-file web/.env.test -f web/docker-compose.test.yaml logs <service-name>

Best practices

  1. Isolation: Each test should be independent and not rely on other tests
  2. Cleanup: Use fixtures and dispose patterns for resource cleanup
  3. Mocking: Mock external dependencies in unit tests
  4. Descriptive Names: Use clear, descriptive test method names
  5. Arrange-Act-Assert: Follow AAA pattern in unit tests
  6. Given-When-Then: Follow GWT pattern in BDD scenarios