As a startup, it's easy to get caught up in the excitement of building a new product. You have a great idea, you've got a team of talented people, and you're ready to change the world. But before you get too far down the road, it's important to take a step back and ask yourself: "Do I even test?"
Testing is an important part of any software development process. It helps ensure that your code works as expected, and it can also help you to catch bugs before they turn into a problem for your users.
Additionally, we're all well aware that no application comes without dependencies. At some point, you need to test contracts between your applications and your API dependencies. This is where mocking comes into play.
Mocking is a technique that allows you to replace real objects with fake ones during testing. In our scenario, we replace specific requests with fake responses.
This can significantly improve your developer experience and confidence to ship features to production, given that writing of such a test is fast and easy.
Another important aspect of mocking is that you don't want to test your application against real third-party APIs, like Stripe, Auth0, or any other API that you don't control. During tests, you really just want to test your application logic in isolation.
In this blog post, we'll discuss why testing is so important and how we build our own testing library to encourage good testing practices within our team and customers.
Some people out there will tell you that testing is a waste of time. They'll say that it's too expensive, or that it slows down development.
But these arguments are based on a misunderstanding of what testing really is. Testing isn't just about making sure your code works; it's about making sure your code does what you want it to do.
And if you don't test your code, then how can you be sure that it does what you want it to do? That simple.
This becomes even more important when you're not the only one working on your codebase and your project has reached a certain size.
This will have a huge impact on your productivity because you don't feel confident to ship features to production without testing them first, and nothing is worse than a codebase that was not built with testing in mind.
This is the turning point where you wish you had tests:
Other good reasons to write tests are that they help you to document the capabilities of your application. E.g., if you're building a GraphQL Query Validator, you can write tests that document which parts of the GraphQL specification you support.
Tests are also very important for open-source projects. Accepting contributions from the community is a double-edged sword. On the one hand, it's great to have more people working on your project.
On the other hand, how can we be sure that these contributions don't break anything? The answer is simple: tests.
Without tests, it's impossible to trust someone with less experience in a codebase to make changes without breaking anything.
Let's take a step back and think about what types of tests exist.
So what is the right start? We believe that you should start with end-to-end tests. Especially, when it comes to API-integration testing.
We've built a testing library that helps you to write end-to-end tests that are easy to read and maintain.
First, let's define some terms:
Let's start by creating a hello country
WunderGraph application. We'll use the following configuration:
const countries = introspect.graphql({
apiNamespace: 'countries',
url: new EnvironmentVariable(
'COUNTRIES_URL',
'https://countries.trevorblades.com/'
),
})
configureWunderGraphApplication({
apis: [countries],
server,
operations,
})
This example configures a GraphQL data-Source that uses the countries’ GraphQL API. We'll create a simple GraphQL operation in .wundergraph/operations/Countries.graphql
and expose the query through the WunderGraph RPC protocol:
query ($code: String) {
countries_countries(filter: { code: { eq: $code } }) {
code
name
}
}
Accessible through the following URL: http://localhost:9991/operations/Countries?code=ES
Next, we need to generate a type-safe client for our operation. We can do this by running the following command in the root directory of our project:
npm exec -- wunderctl generate
This has to be done only once as long as you don't change your WunderGraph configuration.
Now that we have a WunderGraph Gateway, we can write a test in your test runner of choice. Let's start by creating a new file called countries.test.ts
:
import { expect, describe, it, beforeAll } from 'vitest'
import {
createTestAndMockServer,
TestServers,
} from '../.wundergraph/generated/testing'
let ts: TestServers
beforeAll(async () => {
ts = createTestAndMockServer()
return ts.start({
mockURLEnvs: ['COUNTRIES_URL'],
})
})
This code starts your WunderGraph Gateway and a mock server which we will configure programmatically. It also set the COUNTRIES_URL
environment variable to the URL of the mock server. This allows us to use the same configuration for both production and testing environments.
Next, we want to mock the upstream request to the countries API. We can do this by using the mock
method:
describe('Mock http datasource', () => {
it('Should be able to get country based on country code', async () => {
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}}){name code}}'
)
return {
body: {
data: {
countries_countries: [
{
code: 'ES',
name: 'Spain',
},
],
},
},
}
},
})
// see below ...
})
})
Now that we have configured a mocked response, we can use the agenerated Client to make a real request against the http://localhost:9991/operations/Countries
endpoint without starting up the countries API.
// ... see above
const result = await ts.testServer.client().query({
operationName: 'CountryByCode',
input: {
code: 'ES',
},
})
// If the mock was not called or nothing matches, the test will fail
scope.done()
expect(result.error).toBeUndefined()
expect(result.data).toBeDefined()
expect(result.data?.countries_countries?.[0].code).toBe('ES')
We are making a request against the WunderGraph Gateway and checking if the response is correct. If everything works as expected, the test should pass. If not, the test will fail. This simple test covers a lot of ground:
It verifies that custom WunderGraph hooks are working correctly.
Some notes about mocking: While it provides an easy way to speed up our development process and to test edge cases that are hard to reproduce in production, it's not a replacement for real production traffic tests. It's essential that your upstream services have stable contracts.
This allows us to mock the upstream services without having to worry about writing tests against an outdated API specification.
The mock
method allows you to mock any external requests that are made by your WunderGraph Gateway. It takes a single argument that is an object with 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.
You can use your favorite assertion library to verify that the request is correct. In this example, we use the expect
function from vitest
. You can also use jest
or any other assertion library.
If an assertion fails or any error is thrown inside those handlers, the test will fail and the error will be rethrown when calling scope.done()
. This ensures that the test runner can handle the error correctly e.g., by printing a stack trace or showing a diff.
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)
In this article, we've shown you how to use the WunderGraph testing library to make writing tests easier, more adaptable, and more maintainable. If you're interested in learning more about how WunderGraph, check out our documentation or get in touch with us on Twitter or Discord.
Also published here