Skip to main content

Schema coverage

Point ProtoTest at your GraphQL schema and it reports which types, fields, arguments and input fields your suite actually exercised.

CollectorReportsConsumes
GraphQLSchemaCoverageCollector (this page)fields, arguments, input fields per typeeach executed document, walked against the schema
GraphQLCoverageCollector (Operation coverage)one covered GraphQL operation item per operation identifier, with a hit countevery graphql.response observation

A field is covered when a test selects it. An argument is covered when a test passes it. An input field is covered when a test supplies it, including values that arrived through variables. Introspection types (__Schema, __Type, …) are ignored.

What gets reported​

Items appear under the category GraphQL schema, nested like the schema:

GraphQL type Query (no verdict: totals its fields)
└─ GraphQL field orders 3 hits (returnType, deprecated)
└─ GraphQL argument where 1 hit (declared type)
GraphQL input type CreateOrderInput (no verdict: totals its fields)
└─ GraphQL input field product 2 hits (declared type)

Each field, argument and input-field item carries IsCovered and a hit count. Fields add returnType and deprecated metadata, and arguments and input fields carry their declared type. Type rows total their fields' hits and carry no verdict.

One aggregate item records the schema identity: identifier spec, display Schema, metadata spec.source (the configured source) and spec.hash (SHA-256 of the loaded SDL). It has no covered verdict, so the type and field totals ignore it. A cross-run comparison uses it to tell the same schema from a changed one.

Point at the schema​

SourceSnippetNotes
File pathWithSchemaCoverage("control-plane.graphql")copy the file to the output directory (see below)
Inline SDLWithSchemaCoverage(sdl)handy for small test schemas
URL or configurationWithSchemaCoverage() plus the Schema keya relative URL resolves against the application's BaseUrl
Path the application servesWithSchemaCoverage("/graphql?sdl")with no BaseUrl configured, loads from the application once infrastructure started
builder.AddApplication("Api", app => app
.AddGraphQL(graphQL => graphQL
.AddClient("GraphQL")
.WithSchemaCoverage(Path.Combine(AppContext.BaseDirectory, "control-plane.graphql"))));

Leave schemaSource out to read it from configuration instead:

builder.AddApplication("Api", app => app
.AddGraphQL(graphQL => graphQL
.AddClient("GraphQL", "https://api.example.test/graphql")
.WithSchemaCoverage()));
{ "ProtoTest": { "Applications": { "Api": { "GraphQL": { "Schema": "schema.graphql" } } } } }

A relative URL in Schema resolves against the application's BaseUrl. Without a schema from either source, the collector throws InvalidOperationException naming the missing GraphQL:Schema key.

A path that starts with / with no BaseUrl to resolve it against is read from the application itself, in the run's AfterInfrastructureAsync phase before the first test. An in-process application serves its own schema, so no copy is committed:

builder.AddApplication("Api", app => app
.AddAspNetCoreServer<Program>()
.AddGraphQL(graphQL => graphQL
.AddClient("GraphQL")
.WithSchemaCoverage("/graphql?sdl")));

A schema the application does not serve fails the run's start, naming the path.

Ship the schema with the tests

Copy the .graphql file to the output directory so AppContext.BaseDirectory finds it:

<None Include="control-plane.graphql" CopyToOutputDirectory="PreserveNewest" />

WithSchemaCoverage(schemaSource) is shorthand for .AddCollector<GraphQLSchemaCoverageCollector>(schemaSource). The parameterless WithSchemaCoverage() binds the schema from configuration. Every executed document is parsed and walked against the schema, following fragments and inline fragments. Each fragment is walked once per document.

Operation coverage​

GraphQLCoverageCollector is the operation-level collector that sits alongside the schema collector. It aggregates every graphql.response observation into one covered GraphQL operation item per operation identifier, with a hit count. That includes shape-driven, fluent and raw operations and subscription events. Operation names are case-sensitive. Register it with .AddCollector<GraphQLCoverageCollector>(). It ignores other observation kinds such as graphql.contract.shape.

The report is written by whichever sinks you register. See Coverage for the bigger picture.