How to run the test suites and what each one covers.
| Suite | Location | Framework | Type |
|---|---|---|---|
| Backend (unit) | backend/Api.Tests/ |
xUnit + EF Core InMemory | Unit + service-level integration |
| Backend (integration) | backend/Api.IntegrationTests/ |
xUnit + Testcontainers (Postgres) | Real-database integration |
| Frontend | frontend/src/**/*.test.{ts,tsx} |
Vitest + Testing Library | Unit + component render |
Both stacks run in CI on every PR targeting main and every push to main before the
image is built or deployed (test job in .github/workflows/validate.yml and
release.yml).
# From the repo root:
dotnet test backend/Fuel.slnx -c ReleaseThe solution file (backend/Fuel.slnx) references Api, Api.Tests, and
Api.IntegrationTests. A single dotnet test discovers everything. EF Core
InMemory is used for unit tests — no real PostgreSQL needed for Api.Tests.
Api.IntegrationTests spins up a real Postgres container via Testcontainers
(Docker required).
# From the repo root:
npm ci --prefix frontend # first time only
npm test --prefix frontend -- --runOr from inside frontend/:
npm ci
npm test # single run
npm run test:watch # watch modeThese are the starter tests — worked examples of each style (pure logic, EF InMemory DB, controller, background service, frontend unit). Add feature tests alongside them as the product is built out.
| File | What it tests |
|---|---|
BackupServiceTests.cs |
The periodic backup writer: ParseInterval mapping (daily/weekly/monthly/default), RunBackupAsync writing a snapshot with the convention name, no-op when there are no users, that secrets are never written, and retention/pruning to BACKUP_KEEP. Uses EF Core InMemory + a temp dir. |
ReleaseNotifierTests.cs |
BuildEmail (HTML bullet wrapping, encoding, empty notes, plain-text variant) and ExecuteAsync (idempotency via last_notified_version, disabled/dev/missing-version idle paths, best-effort loop where one failure doesn't block others, recipient filter for IsActive && NotifyReleases). Uses a fake IEmailSender and EF Core InMemory. |
UnsubscribeControllerTests.cs |
Token-scoped unsubscribe: a valid token clears the release opt-in, unknown token → 404, empty token → 400. |
UserControllerTests.cs |
The user-prefs endpoints: GET returns current values, unknown user → 404, PUT persists and echoes the new value. |
| File | What it tests |
|---|---|
lib/storage.test.ts |
localStorage helpers: the app: key prefix, getAutoUpdate/getFontScale defaults, font-scale round-trip, malformed-JSON resilience, and theme save/read. |
| File | What it tests |
|---|---|
EntryIntegrationTests.cs |
GET entries?from=&to= timestamptz range round-trip on real Postgres (the exact case InMemory missed); ordering within a window. Inputs are Utc-kind and the in-window entry sits strictly inside the range, so the result is timezone-independent. |
FoodIntegrationTests.cs |
Composite-food IngredientCount / IsComposite; the three per-user sort modes (priority / most-used / recent) — the GroupJoin + aggregate + null-coalescing OrderBy SQL InMemory translates differently from Npgsql; and cycle detection (FoodService.WouldCreateCycle) over a real ingredient graph. |
EstimatorIntegrationTests.cs |
Full HTTP round-trips for OpenAI and Anthropic estimators via WireMock.Net: successful text/image parsing, auth headers, thinking-block skipping, malformed inner JSON → AiUnavailableException, server errors → HttpRequestException, empty-item filtering, connection refusal. |
EstimatorChainIntegrationTests.cs |
The ordered multi-provider registry (EstimatorChain) over real HTTP: fall-through when the first provider is unreachable, Order precedence, all-providers-fail → AiUnavailableException, capability filtering (text-only providers reject a vision request without calling upstream), and disabled-provider skipping. |
PostgresFixture.cs |
xUnit collection fixture: spins one Testcontainers Postgres container, applies migrations, yields fresh AppDbContext per test. ResetAsync() truncates all data tables so a test can assert on whole-table counts/ordering without cross-test pollution (the collection runs sequentially). |
WireMockFixture.cs |
xUnit collection fixture: starts one WireMock.Net server on a random port; provides a convenience Connection() helper. Estimator test classes call Server.Reset() in their constructor so per-test mappings don't accumulate on the shared server. |
Now built as backend/Api.IntegrationTests/. The two reasons it exists:
- EF InMemory is not Postgres. It silently ignores provider-specific rules, so a
green unit suite can still ship a persistence bug. Concrete examples already hit:
GetEntriespassed aDateTimeKind.Unspecifiedvalue, which Npgsql rejects against atimestamptzcolumn (InMemory accepted it);ToListItem(…)inside a LINQSelectprojection broke ingredient-count translation on real Npgsql while InMemory hid the regression. - External calls (
INutritionEstimator→ AI providers). The AI add-methods make outbound HTTP that should be tested against a stub/recorded server (capability flags, timeouts, malformed responses, the text-only vs multimodal split) — without hitting the real provider or spending tokens in CI. (WireMock.Net is included as a dependency for this.)
Shape:
- Separate
Api.IntegrationTestsproject in the same solution —dotnet testdiscovers it alongside unit tests. - Real Postgres via Testcontainers (
postgres:15-alpine). - External HTTP stubs via WireMock.Net (estimator + provider-chain tests).
- No
[Trait]filter by default — the CI runner has Docker and runs both tiers.
To test internal members without exposing them publicly, the Api project declares:
<InternalsVisibleTo Include="Api.Tests" />This lets the test project call internal methods directly (e.g.
BackupService.ParseInterval, ReleaseNotifier.BuildEmail) while keeping them
hidden from external consumers.
The test job runs on every PR targeting main (.github/workflows/validate.yml)
and every push to main (.github/workflows/release.yml). In both, build and the
deploy job are gated behind it (build → needs: test, deploy → needs: build), so
a red suite blocks the image from being built or deployed.