Skip to content

Testing

Ampora has three test layers. Each layer answers a different question.

Unit tests

Live alongside the code they test. They run in milliseconds, mock out persistence, and exercise one class at a time.

dotnet test --filter "FullyQualifiedName!~Ampora.Integration.Tests"

Use them for:

  • domain logic (RolloutStateMachine, PolicyEvaluator),
  • algorithms (DSL parser, semantic-diff engine, dynamic-group matcher),
  • formatters and parsers (YAML normaliser, OpAMP frame codec).

Avoid them for:

  • anything that cares about real EF behaviour (migrations, JSONB containment),
  • multi-class flows ("operator pushes config and agent acks"),
  • HTTP / WebSocket round-trips.

Integration tests

Live in tests/Ampora.Integration.Tests/. They use Testcontainers to spin up a real PostgreSQL and exercise the application service layer end-to-end.

dotnet test tests/Ampora.Integration.Tests/Ampora.Integration.Tests.csproj

Integration tests:

  • exercise the real DbContext and migrations,
  • exercise OpAMP wire-format round-trips,
  • exercise the dispatch backplane (in-process, Postgres, Redis-via-Testcontainers),
  • exercise the leader-election lease lifecycle.

They are slower (multi-second) and require Docker. CI runs them on every MR.

Important detail

When you write a fleet test that constructs AmporaDbContext directly, do not pass a tenant scope to the constructor — use new AmporaDbContext(opts) without tenant. The cached EF model is process-wide and a tenant-scoped construction breaks sibling tests that expect a clean model.

Field tests

Live in tests/Ampora.FieldTests/ (when present). Tagged with [Trait("category", "field")]. They reach out to real OpenTelemetry Collector binaries and run package transfer flows for real, etc.

CI does not run these on every push. They run nightly against a staging cluster.

dotnet test --filter "category=field"

Property-based tests

We use FsCheck for the OpAMP wire format and the YAML normaliser where the input space is too large for example-based tests:

  • "any well-formed AgentToServer round-trips through encode/decode".
  • "two YAMLs with the same semantic AST produce the same semantic hash, regardless of whitespace / anchor expansion".

Property tests live next to the code; they are not a separate test layer.

Coverage

Coverage is collected via coverlet.collector and reported as Cobertura. CI publishes coverage; the project floor is in the contributor handbook (currently 75 % overall, with hot paths required to be > 90 %).

Mocking

Moq is the default. Avoid mocking what you do not own — for external libraries, use a thin adapter and mock the adapter.

For DbContext-using tests, prefer the integration layer; SQLite in-memory is not a faithful substitute for Postgres JSONB behaviour.

Async tests

All async tests await rather than .Result. Use TestCancellationToken.Source.Token so the test framework can cancel runaway tasks.

Test data

Builders in tests/_helpers/builders/ produce realistic entities. Don't hand-roll new Agent { ... } from scratch — call AgentBuilder.New().WithCapabilities(...).Build() so the next change to the entity does not break every test.