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. The Right Portion of Tests 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. These are the most basic type of test. They check individual functions or classes. Unit tests tend to be very specific and detailed. They are great for catching bugs early on in the development process, but they can also be difficult to maintain over time. If you change one line of code, you might have to update dozens of unit tests. This makes it hard to keep track of all the changes you've made and makes it even harder to figure out which tests need updating. Unit tests: These are similar to unit tests except they check how multiple components work together. Integration tests tend to be less detailed than unit tests. They are great for catching bugs that might not show up until you've composed multiple components. Integration tests: These are the most comprehensive type of test. They check how your entire application works from start to finish. End-to-end tests are great for catching bugs that might not show up until you've deployed your application into production. They are usually written against high-level requirements and are therefore less detailed than unit tests or integration tests and easier to write and maintain. They can be a very powerful tool for low-cost, high-impact testing. End-to-end tests: So what is the right start? We believe that you should start with . Especially, when it comes to API-integration testing. end-to-end tests 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: An instance of a WunderGraph application. It's responsible for handling requests from clients to multiple upstream services. Each upstream can be considered as a dependency. WunderGraph Gateway: A data-source is a single upstream service that is integrated into your WunderGraph Gateway. It can be a REST API, GraphQL API, or any other type of service. WunderGraph Data-Source: A test client is an auto-generated client from your WunderGraph Gateway. It's a simple HTTP client that allows you to send requests to your WunderGraph Gateway in a type-safe manner. Client: A test runner is a tool that runs your tests. In this example, we use Vitess, but you can use any other test runner. Test runner: Creating a WunderNode Let's start by creating a WunderGraph application. We'll use the following configuration: hello country 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 ’ GraphQL API. We'll create a simple GraphQL operation in and expose the query through the WunderGraph RPC protocol: countries .wundergraph/operations/Countries.graphql query ($code: String) { countries_countries(filter: { code: { eq: $code } }) { code name } } Accessible through the following URL: http://localhost:9991/operations/Countries?code=ES Generate a Client 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. Writing a Test 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 environment variable to the URL of the mock server. This allows us to use the same configuration for both production and testing environments. COUNTRIES_URL Next, we want to mock the upstream request to the countries API. We can do this by using the method: mock 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 endpoint without starting up the countries API. http://localhost:9991/operations/Countries // ... 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 the WunderNode is started correctly, and the generated code is compliant with the current API specification. It verifies that the WunderNode is able to handle requests from clients. It verifies that custom WunderGraph hooks are working correctly. 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. Some notes about mocking: This allows us to mock the upstream services without having to worry about writing tests against an outdated API specification. Mock Implementation The 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: mock - A function that returns true if the request matches. match - A function that returns the response or throws an error. handler - The number of times the mock should be called. Defaults to 1. times - 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 after the test. Defaults to false. persist ts.mockServer.reset() You can use your favorite assertion library to verify that the request is correct. In this example, we use the function from . You can also use or any other assertion library. expect vitest jest If an assertion fails or any error is thrown inside those handlers, the test will fail and the error will be rethrown when calling . This ensures that the test runner can handle the error correctly e.g., by printing a stack trace or showing a diff. scope.done() 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) Conclusion 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