Testing strategy
QueryGuard runs on a hot, concurrent, provider-dependent path, and its whole value proposition is that its output can be trusted. So the test suite has to prove five things before any claim is made publicly: behavior, isolation, privacy, report stability, and honest performance.
Layers
| Layer | Project | Proves | Required on PR |
|---|---|---|---|
| Contract / unit | QueryGuard.Core.Tests |
Models, policy precedence, severity, ordering, lifecycle invariants | Yes |
| Fingerprint approval | QueryGuard.Core.Tests, QueryGuard.ProviderTests |
Normalization and fingerprint IDs do not drift | Yes |
| EF interception | QueryGuard.EntityFrameworkCore.Tests |
Real relational capture, sync and async, failures, no-scope silence | Yes |
| ASP.NET integration | QueryGuard.AspNetCore.Tests |
Request lifecycle, route policy resolution, response equivalence | Yes |
| Testing API | QueryGuard.Testing.Tests |
Scope semantics and assertion message quality | Yes |
| Reporter / schema | QueryGuard.Reporting.Tests |
JSON schema, JUnit validity, log event IDs, redaction | Yes |
| Concurrency stress | QueryGuard.Core.Tests, QueryGuard.AspNetCore.Tests |
Zero cross-session leakage under parallel load | Yes |
| Provider integration | QueryGuard.ProviderTests |
Provider-specific SQL and capture behavior | Yes: SQLite, PostgreSQL, SQL Server |
| Performance | QueryGuard.Benchmarks |
Measured hot-path cost | Smoke only |
| Package consumer smoke | package-validate job |
The built package really works as a package | Yes |
Critical scenarios
These are the behaviors that must never regress. Each maps to at least one test.
Scope
- No active session: the interceptor captures nothing and application behavior is unchanged.
- Two parallel scopes with different query counts: each result contains only its own records.
- A nested scope completing after an exception: the parent session is restored.
Interception
- A synchronous
ToList(): one reader record with a plausible duration and command kind. - An asynchronous
ToListAsync(): an equivalent record, with cancellation honored. - A failing command: failure evidence is recorded and the original exception stays primary, with its type and stack intact.
- Scalar and non-query commands are distinguishable from reader commands.
Fingerprinting
- The same SQL with different whitespace or comments produces the same fingerprint.
- The same EF query with different generated parameter names produces the same fingerprint, per provider fixture.
- A
QueryGuard:Ignoretag survives comment stripping and marks the finding ignored: visibly, with its reason, never silently dropped. - Fingerprint IDs are identical across runs, processes, and both target frameworks.
Detection and budgets
- Two occurrences with a threshold of three: no warning. Three: a candidate warning. Boundaries are tested from both sides, because off-by-one here means false positives for everyone.
- Total queries exactly at the limit: pass. One over: the configured severity, with expected and actual values in the result.
- A per-fingerprint budget breach identifies the fingerprint, the allowed count, the actual count, and a sample.
- The duration budget is disabled by default and does not fire even when duration is measured.
- An endpoint-specific policy wins over the default, in a documented resolution order.
- An allowlisted fingerprint remains visible as ignored, with its reason.
Privacy
- Default configuration: no parameter values, no connection strings, anywhere in the result or in any reporter's output.
- Stack traces off by default: no capture, no allocation.
- Stack traces enabled: exactly one filtered trace per fingerprint group, not one per command.
- Every reporter's output passes the same redaction assertions: a reporter cannot bypass the central policy.
ASP.NET Core
- Two concurrent requests to different routes: isolated sessions, policies named by route pattern.
- An endpoint that throws after a query: the session is finalized and logged, and the original exception pipeline is intact.
- Response equivalence with QueryGuard enabled and disabled: same status, same body, same headers.
Reporting
- JSON output is deterministic and carries
schemaVersion; ignored and failed findings both appear. - JUnit XML is valid and renders a meaningful test case and failure in common CI viewers.
End to end
- The sample's buggy endpoint fails its repeated-query budget; the fixed endpoint returns an equivalent response and passes.
Rules
Determinism. Results are immutable and deterministically ordered so snapshots are meaningful. A test that depends on collection ordering it did not assert is a latent flake.
No blind retries on correctness tests. A flaky isolation test is a real bug in either the code or the test, and retrying it hides the most expensive class of defect QueryGuard can have. Retry logic is acceptable only for container startup, never for an assertion.
Timing tests use generous bounds. CI machines are noisy. Duration-based tests assert "a duration was measured and is positive", not "under 50 ms". The duration budget feature is off by default for the same reason.
Approval fixtures are reviewed, not regenerated. When a fingerprint fixture changes, the diff is the review. Blindly accepting new output defeats the point.
Docker is optional locally, required in CI. The PostgreSQL suite skips itself when Docker is unavailable so a contributor is never blocked, and runs for real in CI so coverage is not optional in practice.
Both target frameworks are tested, not just built. The reason for multi-targeting is EF Core behavior differences, and a compile-only pass would not see them.
Benchmark honesty rules
Benchmarks exist to answer "what does this cost?", not to produce a marketing number.
| Scenario | Baseline | Variant | Measures |
|---|---|---|---|
| Interceptor, no active scope | EF query without QueryGuard | Interceptor registered, no session | Cost of merely being installed |
| Capture only | No QueryGuard | Active session, fingerprinting disabled | Cost of recording |
| Capture + fingerprint | Capture only | Full normalization and hashing | Cost of grouping |
| First stack trace | Capture + fingerprint | One filtered trace per fingerprint | Cost of the optional evidence |
| Reporter finalization | Completed result, no output | JSON / JUnit / logger written | Post-scope cost |
Every published result discloses CPU, OS, .NET version, EF Core version, provider, scenario configuration, sample size, and the source commit, with the raw BenchmarkDotNet artifacts attached.
Claims that are never made: "zero overhead", "negligible cost", "fastest EF Core profiler", or any production latency conclusion drawn from a microbenchmark.