Call GraphQL services
The graphql pack sends queries, mutations and subscriptions to GraphQL services and checks what they answer. A federated graph’s gateway and a subgraph alone are GraphQL services alike: register each at its URL, and the same steps test your subgraph on its own and the graph through the gateway.
Scenario: A parcel through the graph comes with its shop Given a seeds/graph-shop.yaml db seed And the graph graphql service with the following properties: | url | http://localhost:4000/graphql | | schema | introspection | When the graphql/parcel-with-shop.graphql query is sent to the graph graphql service with the following variables: | reference | PX-GQL-7205 | Then the graph graphql service answered without errors And the graph graphql service's data has the following properties: | parcel.status | IN_TRANSIT | | parcel.shop.name | Alder & Ash |Add the pack to the project with axx pack add graphql (Choose packs).
Register the service
Section titled “Register the service”| Property | What it is |
|---|---|
url |
The URL operations are posted to (required). |
schema |
Its schema: an SDL file of the project, a URL, or introspection to ask the service. Operations and answers are checked against it. |
header.<name> |
A header sent with every operation, such as header.Authorization. |
timeout |
How long an operation may take. Default 10s. |
subscriptions |
graphql-ws (the default: the graphql-transport-ws protocol) or sse (server-sent events). |
subscriptions url |
Where subscriptions go. Default: the url, with ws:// or wss:// for graphql-ws. |
${env:..} values are masked in logs and failures, and ${token:..} names a token the scenario registered (Test webhooks and tokens). A subgraph’s SDL may use the federation directives (@key, @link…).
Send queries and mutations
Section titled “Send queries and mutations”Keep operations in .graphql files, and give their variables in a table:
mutation HoldParcel($reference: ID!, $until: String!, $reason: String) { holdParcel(reference: $reference, until: $until, reason: $reason) { reference status heldUntil }}When the graphql/hold-parcel.graphql mutation is sent to the parcels graphql service with the following variables: | reference | PX-GQL-7202 | | until | 2026-10-05 | | reason | recipient on holiday |When a query is sent to the parcels graphql service: """ { parcel(reference: "PX-GQL-7201") { status } } """- A variable takes the type the operation declares:
10115is text for aString!variable and a number for anInt!. Double quotes make a value text,nullis null, andundefinedleaves the variable out. A row can be a path into an input object (address.postcode). - The step says what the operation is: sending a mutation file with “query” fails, which keeps the feature file honest about what it does.
- Errors are the answer’s: an answer with errors does not fail the step that sends the operation. Check them.
Check the answer
Section titled “Check the answer”Then the parcels graphql service answered without errorsAnd the parcels graphql service's data has the following properties: | holdParcel.status | ON_HOLD | | holdParcel.heldUntil | 2026-10-05 |Then the parcels graphql service answered an error where: | path | holdParcel | | extensions.code | NOT_HOLDABLE |- Data paths are dotted (
parcel.shop.name) or JSONPath;nullandundefinedwork as in the other property steps. - An error’s
pathis its fields and indexes joined with dots:parcel.shop.name,parcels.0.reference. The check passes when one of the answer’s errors has every row. - The checks read the service’s last answer in the scenario.
Subscriptions
Section titled “Subscriptions”Start a subscription before what makes the service send its messages:
Scenario: A shop's app hears its parcel go out for delivery Given a seeds/graph-scanned.yaml db seed And the graphql/parcel-scanned.graphql subscription is started on the parcels graphql service with the following variables: | reference | PX-GQL-7204 | And a depot-scans kafka event And the depot-scans kafka event key is PX-GQL-7204 And the depot-scans kafka event payload is a kafka/scan-out-for-delivery.json resource When the depot-scans kafka event is published using schema schemas/depot-scan.avsc Then within 20s the parcels graphql service's subscription received a message where: | parcelScanned.status | OUT_FOR_DELIVERY |- The check waits for a message whose data has those values: 10 seconds, or
within {duration}. When the subscription has ended, it fails at once, saying why. - A subscription belongs to the scenario, which ends it when it ends.
Check against the schema
Section titled “Check against the schema”With a schema row, every operation and its variables are checked against the schema before they are sent, and every answer’s data against the operation’s types. An operation that breaks the schema is not sent; the step fails and names the rule it broke:
| Key | Found when |
|---|---|
validation.operation.<rule> |
the operation breaks a validation rule of the GraphQL spec (FieldsOnCorrectType, KnownArgumentNames, ProvidedRequiredArguments…) |
validation.variables.missing |
a required variable is missing |
validation.variables.type |
a variable is not of its type |
validation.response.nonNull |
a non-null field is null, without an error that explains it |
validation.response.type |
a field’s value is not of its type |
validation.response.enum |
a value is not one of its enum’s |
validation.response.missingField |
a field the operation selects is missing |
validation.response.unknownField |
the data has a field the operation does not select |
ERROR fails the step, WARN and INFO log the finding, and IGNORE drops it. A scenario that sends a broken operation on purpose, to see how the service refuses it, relaxes only what it breaks:
Scenario: The graph refuses a field it does not have Given the GraphQL validation levels are: | validation.operation.FieldsOnCorrectType | IGNORE | When a query is sent to the graph graphql service: """ query SignedBy { parcel(reference: "PX-GQL-7299") { signedBy } } """ Then the graph graphql service answered an error where: | message | Cannot query field "signedBy" on type "Parcel". |A key also sets the keys below it (validation.response covers validation.response.enum), and the most specific key set wins. packs.graphql.levels in axx.yaml sets the defaults the scenarios’ levels are merged over.
Federated graphs
Section titled “Federated graphs”A federated graph has three parts to test, with the same steps:
- Your subgraph on its own: register it at its own URL. Queries, mutations and subscriptions reach your code directly, and its SDL is the contract.
- The graph through its gateway: register the gateway’s URL. A query crosses your subgraph and the others, as the gateway plans it, which is where composition and entity resolution break.
- Your service as a client of someone’s graph: mock that graph, and check what your service asked it.
In the example, a gateway (Hive Gateway) composes the service’s own subgraph with a shops subgraph that another team owns, mocked by WireMock (below). Axx does not include a gateway: it is a container in your compose.yaml, like the one you run in production (Apollo Router, Cosmo Router, Hive Gateway…), and Axx only talks to its URL.
Mock GraphQL services and subgraphs
Section titled “Mock GraphQL services and subgraphs”The Axx WireMock image mocks GraphQL services and subgraphs field by field, from their schema: it runs each operation against the schema, so any query your service or a gateway sends is answered, whatever fields it selects. A subgraph’s mock answers _service and _entities, so a gateway composes and plans with it as with the real one. See Mock GraphQL services and subgraphs.
See the graphql pack’s reference for every step.