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.
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.
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.
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
AgentToServerround-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.