Testing with QueryGuard

QueryGuard can measure a real ASP.NET Core request or a smaller block of application code. Both paths produce the same QueryGuardResult and use the same assertions.

ASP.NET Core integration tests

Install the helper package:

dotnet add package QueryGuard.AspNetCore.Testing

Measure a request made through WebApplicationFactory:

using Microsoft.AspNetCore.Mvc.Testing;
using QueryGuard.AspNetCore.Testing;
using QueryGuard.Testing;

using var factory = new WebApplicationFactory<Program>();
await using var guard = factory.TrackQueries<Program, AppDbContext>(
    "GET /api/companies",
    QueryGuardPolicy.Create("companies")
        .WithMaxOccurrencesPerFingerprint(5));

var response = await guard.Client.GetAsync("/api/companies");
response.EnsureSuccessStatusCode();

var result = await guard.CompleteAsync();
QueryGuardAssert.Passes(result);

TrackQueries handles the setup that is easy to miss in an integration test:

  • it attaches QueryGuard to the selected DbContext
  • it sets TestServerOptions.PreserveExecutionContext
  • it uses the session accessor from the hosted application
  • it disables QueryGuard request middleware for the measurement, so there is only one active scope
  • it avoids adding the interceptor twice when the application already uses QueryGuard

Use the client exposed by guard. A client created directly from the original factory does not use the configured test host.

If the application uses more than one context, choose the context whose commands the test should measure. Open separate measurements when a test needs separate budgets for separate contexts.

Services and background jobs

Install the general testing package:

dotnet add package QueryGuard.Testing

Attach QueryGuard when the context is configured:

options.UseSqlite(connectionString).UseQueryGuard();

Then open a scope around the code under test:

await using var scope = QueryGuardScope.Start(
    "refresh company summary",
    QueryGuardPolicy.Create("company-summary")
        .WithMaxQueries(3)
        .WithMaxOccurrencesPerFingerprint(1));

await service.RefreshCompanySummaryAsync();

var result = await scope.CompleteAsync();
QueryGuardAssert.Passes(result);

QueryGuard.Testing brings the EF Core integration with it. It does not reference xUnit, NUnit, MSTest, or TUnit.

Start with a baseline when the budget is unknown

A new test often has no agreed query budget. Record the current result first and compare later runs:

var baseline = QueryGuardBaseline.FromJson(await File.ReadAllTextAsync("queryguard-baseline.json"));
var comparison = QueryGuardBaselineComparison.Compare(baseline, [result]);

Assert.Empty(comparison.Regressions);

See baselines for recording the file and publishing the comparison in CI.

Common failures

Symptom Check
Zero commands from a WebApplicationFactory request Use TrackQueries and its Client
Twice the expected command count Do not run request middleware inside an explicit test measurement
Commands appear in the wrong scope Complete one measurement before opening the next one
The test passes but the report is missing in CI Write reports to a path anchored at the repository root

See troubleshooting for manual session wiring and lower-level details.