Mocking

Mocking

WunderGraph testing library comes with builtin support for mocking your HTTP datasources or APIs. This allows you to test your application without having to rely on external services.

Our testing library is framework agnostic, so you can use it with your preferred test framework. We use Vitest in this guide. It is a Jest Compatible test runner, but it should be easily adaptable to any testing framework.

Initial setup

Make sure your wundergraph.config.ts is set up to generate the WunderGraph testing library. Take a closer look at the url. We use an environment variable to set the URL of the datasource. This allows us to replace the URL with the mock server URL when running the tests.

1
2
3
4
5
6
7
8
9
10
11
12
13
const countries = introspect.graphql({
apiNamespace: 'countries',
url: new EnvironmentVariable('COUNTRIES_URL', 'https://countries.trevorblades.com/'),
});
configureWunderGraphApplication({
apis: [countries],
server,
operations,
generate: {
codeGenerators: [],
},
});

Test setup

Within a test, you can import createTestAndMockServer() to create the test and mock server instances. Note that depending on your project's settings, the path might be slightly different depending on where you generate your templates. We use beforeAll to start the test and mock server instances. The ts.start() method returns a cleanup function to shutdown all servers. Alternatively, you can call ts.stop(). In vitest you can return an async function to do a cleanup. We also use mockURLEnvs to replace the environment variables with the mock server URL.

Before you run the tests and import the createTestServer function, you must run wundergraph generate --env .env.local to generate the testing library. Some testing frameworks like Vitest or Jest provides a globalSetup hook that allows you to run a script before running the tests. In that way, you can automate it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createTestAndMockServer, TestServers } from '../.wundergraph/generated/testing';
let ts: TestServers;
beforeAll(async () => {
ts = createTestAndMockServer({
// The directory where your wundergraph directory is located
// This is only needed if you run the tests from a different directory than your wundergraph directory
// __dirname is the directory of the current file.
dir: join(__dirname, '..'),
});
return ts.start({
// Environment variables replaced by the test mock server URL
mockURLEnvs: ['COUNTRIES_URL'],
});
});

Environment variables

WunderGraph has builtin support for loading environment variables from a .env file. For testing, we recommend creating a .env.test file to set the environment variables for the test environment. This allows you to set the environment variables for the test environment without affecting your local development environment. The test server search first for a .env.test file, and if it doesn't exist, it will fall back to .env. You can also pass environment variables to the test server with the env option of the createTestAndMockServer method. None of these files should be committed to git. In your CI, you should use plain environment variables that come from your secret store.

Writing tests

We recommended creating as few testing server instances as possible. These minimizes the number of times the server starts and stops, making your tests faster.

Due to the nature of the mock server, it is not possible right now to run multiple tests it concurrently. If you have a demanding test suite, you can create multiple test files and vitest will run them in parallel by default.

Mocking HTTP datasources

When you setup a mock with mock() and the request matches, the mock server will return the response. If the request does not match, the mock server will return a 404 response and the call to scope.done() will fail the test. You have also the ability to throw an error inside the response function to fail the mock. This is useful if you want to verify with test assertion that the request is correct. A thrown error is handled as an unmatched request and the next mock will be checked.

The first argument of the mock function is an object that accepts the following properties:

  • match - A function that returns true if the request matches
  • handler - A function that returns the response or throws an error
  • times - The number of times the mock should be called. Defaults to 1.
  • persist - If true, the mock will not be removed after any number of calls. Be careful with this option, as it can lead to unexpected results if you forget to remove the mock with ts.mockServer.reset() after the test. Defaults to false.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const scope = ts.mockServer.mock({
match: ({ url, method }) => {
return url.path === '/' && method === 'POST';
},
handler: async ({ json }) => {
const body = await json();
expect(body.variables.code).toEqual('ES');
expect(body.query).toEqual(
'query($code: String){countries_countries: countries(filter: {code: {eq: $code}}){code name capital}}'
);
return {
status: 200,
headers: {
'X-Foo': 'Bar',
},
body: {
data: {
countries_countries: [
{
code: 'ES',
name: 'Spain',
capital: 'Madrid',
},
],
},
},
};
},
});

Make a request and validate the mock

The createTestAndMockServer returns a WunderGraphTestServer object that wraps the test server and the auto-generated type-safe client.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const result = await ts.testServer.client().query({
operationName: 'CountryByCode',
input: {
code: 'ES',
},
});
/**
* Make sure to call scope.done() to verify that the request have been made
* If the request was not made, the test will fail
*/
scope.done();
expect(result.error).toBeUndefined();
expect(result.data).toBeDefined();
expect(result.data?.countries_countries?.[0].capital).toBe('Madrid');

If an assertion fails or any error is thrown inside the handlers, the test will fail and the error will be rethrown when calling scope.done(). This ensure that the test runner can handle the error correctly e.g. by printing a stack trace or showing a diff.

1
2
3
4
5
6
7
AssertionError: expected 'ES' to deeply equal 'DE'
Caused by: No mock matched for request POST http://0.0.0.0:36331/
Expected :DE
Actual :ES
<Click to see difference>
at Object.handler (/c/app/countries.test.ts:29:33)

For a full example please check the example in the WunderGraph repository

Summary

This guide showed how easy it is to mock HTTP datasources. It gives you power to write integration tests fast and reliable. You can apply the same principles to mock any HTTP API in your TypeScript functions.

Was this article helpful to you?
Provide feedback

Edit this page