Two test layers: unit tests for business logic in isolation, integration tests for HTTP endpoints against real infrastructure (PostgreSQL + Redis + RabbitMQ via Testcontainers).
Target: Application + Infrastructure logic with no external dependencies.
Infrastructure: EF Core InMemory provider. No Docker required.
Run:
dotnet test backend/tests/Loopless.UnitTests/| Area | Tests | Notes |
|---|---|---|
| Email job | Email/UnreadMessageEmailJobTests.cs |
4 tests — threshold, idempotency, opt-out, read guard |
| DLQ behavior | Email/DlqBehaviorTests.cs |
4 tests — naming, invalid JSON, missing user, send failure |
| Standup features | Standups/ |
Handler + validator tests |
| Messaging | Messaging/ |
Handler + validator tests |
Constraints:
- Use
TestAppDbContext.CreateInMemory()for all DB access. ExecuteUpdateAsync/ExecuteDeleteAsyncnot supported by InMemory — use change-tracking pattern instead.- All RabbitMQ/email interactions go through
IEventPublisher+IEmailSender— inject fakes via constructor.
Target: Full HTTP request/response cycle against a real PostgreSQL database (pgvector enabled).
Infrastructure: Testcontainers (pgvector/pgvector:pg16). Requires Docker Desktop or Docker Engine running locally and in CI.
Run:
dotnet test backend/tests/Loopless.IntegrationTests/CI: Docker socket is available in the GitHub Actions
ubuntu-latestrunner — no extra setup needed.
WebApplicationFactory<Program> + IAsyncLifetime. Spins up a real Postgres container, runs EF migrations, and replaces Keycloak JWT validation with TestAuthHandler.
Authenticated clients: factory.CreateAuthenticatedClient(keycloakId, role) — sets sub claim to keycloakId, role claim to role.
| File | Scenarios |
|---|---|
MatchingEndpointTests.cs |
auth-required, enterprise user discovers freelancers (real pgvector embeddings), semantic search returns 200 |
StandupAnalyticsTests.cs |
auth-required, analytics aggregates seeded standups, empty project returns empty memberCompletions |
MessagingFlowTests.cs |
auth-required, send message persists and appears in GET history, pagination returns paged result |
Seed data directly via AppDbContext obtained from factory.Services.CreateAsyncScope(). Do not seed through the API unless the test specifically covers the creation endpoint.
await using var scope = factory.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Users.Add(user);
await db.SaveChangesAsync();Both test suites run in the quality job:
- name: Run backend unit tests
run: dotnet test backend/tests/Loopless.UnitTests/ --no-build
- name: Run backend integration tests
run: dotnet test backend/tests/Loopless.IntegrationTests/ --no-buildThe integration test job needs Docker — the ubuntu-latest runner has it available by default.
| Layer | Target |
|---|---|
| Application handlers | ≥ 80 % line coverage via unit tests |
| API endpoints (HTTP) | 100 % of endpoint files covered by at least one integration test |
| Email pipeline | 100 % of job + consumer paths (happy + DLQ paths) |