New from the DevTools team: Stresseur, the AI test engineer for APIs →
DevTools
Back to Blog
Abstract illustration of two connected nodes passing a small shape along a line

How to Test a GraphQL API With Multi-Step Flows

Mustafa BayramogluMustafa Bayramoglu

Short answer: test a GraphQL API with multi-step flows by sending the first query and reading a value from response.body.data. Pass it into the next request, and assert on both data and errors at each step. Then run the flow file from the command line in CI, where the CLI exits with 1 when a flow fails.

This guide builds that test in four steps, using the GraphQL request and flow node documented in the DevTools GraphQL docs.

The four-step shape of a GraphQL flow test

A multi-step test is a sequence of requests where a later one depends on an earlier response, such as fetching a user and then loading that user's profile. DevTools ships a GraphQL request editor alongside its HTTP editor, and you can place a GraphQL node in a flow next to ordinary HTTP requests.

The test has four parts:

  1. Create a GraphQL request with a query, variables and headers.
  2. Pass a value from its response into the next request.
  3. Assert on the data and errors of the response as well as the status.
  4. Run the flow in CI and read the JUnit report.

The same shape applies to any chained API test, which the guide on API chain testing covers for REST. GraphQL changes the details of steps two and three, so those get the most space below.

Step 1: Create a GraphQL request with query, variables and headers

Create a new file of type GraphQL in the workspace sidebar. It opens in its own tab with four panes: Query, Variables, Headers and Assertion.

The Query pane holds the operation. The Variables pane holds a JSON object that the query references with $name, and the Headers pane is where Authorization or X-Api-Key goes. The docs use the public countries GraphQL endpoint as their example, which makes it a safe place to practise.

A query that takes a variable looks like this:

query GetCountry($code: ID!) {
  country(code: $code) {
    name
    emoji
    capital
  }
}

In the Variables pane, the value can come from your environment instead of being typed in:

{
  "code": "{{country_code}}"
}

{{country_code}} is resolved from the active environment when the request is sent, so the value follows whichever environment is active. Environment-driven values also suit a file kept in version control, as covered in keeping API tests in version control.

Step 2: Pass a value from one GraphQL response into the next request

A GraphQL response is read through the step's name. The payload always sits under .data, so a later step reads {{NodeName.response.body.data.<field>}}.

The docs show the pattern with a GraphQL step followed by an HTTP request:

- graphql:
    name: GetMe
    url: '{{API_URL}}'
    query: 'query { me { id email } }'

- request:
    name: FetchProfile
    method: GET
    url: '{{API_URL}}/users/{{GetMe.response.body.data.me.id}}'
    depends_on: GetMe

GetMe runs the query. FetchProfile builds its URL from GetMe.response.body.data.me.id, and depends_on: GetMe makes it wait. The depends_on field is what enforces execution order in a flow.

The value can go the other direction too. Variables can reference node outputs, so a value read from one GraphQL response can feed the Variables pane of the next GraphQL step. When a flow has several steps, name each one for what it returns, such as GetMe instead of Step1, because the name becomes part of every reference after it.

Step 3: Assert on data and errors as well as status

GraphQL servers commonly answer 200 even when the operation fails, and the failure lands in response.body.errors. The GraphQL documentation on serving over HTTP gives the reason: HTTP has no status code for partial success, so a response that contains a data key can carry a 2xx status even when it includes errors. A status check alone would pass on a response like that. The docs say to assert on .data and .errors along with the status.

An assertion that covers all three looks like this:

response.status == 200 &&
!response.body.errors &&
response.body.data.countries.length > 0

It requires a 200, no errors array, and a non-empty list under data. Put it in the Assertion pane of each GraphQL step so a failure points at the step that caused it. For more assertion shapes, see the JSON assertion patterns guide.

Step 4: Run the flow in CI and read the JUnit report

Run the YAML file with the CLI reference commands. devtools flow run ./tests/api-tests.yaml UserFlow runs the flow named UserFlow from that file. Add --report junit:results.xml to write a JUnit XML report that CI systems can read.

In GitHub Actions the job installs the CLI with the install script, as in the CI integrations docs, and then runs the flow file with a JUnit report (the docs' example uses api-tests.yaml and test-results.xml):

- name: Install DevTools CLI
  run: curl -fsSL https://sh.dev.tools/install.sh | bash

- name: Run API Tests
  run: |
    devtools flow run api-tests.yaml \
      --report junit:test-results.xml

The CLI exits with 0 when all flows succeed and 1 when one or more fail, so a failing GraphQL assertion fails the job without extra scripting. For publishing the XML as test results, the JUnit reports guide shows how to surface failures in GitHub Actions.

Conclusion

A GraphQL multi-step test is a short chain: query, read from data, pass it on, assert on data and errors, and let the CLI exit code decide the build. Start with one two-step flow against a staging endpoint, add the assertion from step three, and commit the file next to the code it tests.