The Do's and Don'ts of Testing Apollo in React
Adam Hannigan
Engineering Team LeadAs the Apollo ecosystem evolves we find ourselves relying extensively on the underlying functionality it provides. The recent version 3 has highlighted this with the introduction of domain driven type policies and local state management. This enables engineers to consolidate business logic, centralise state and perform advanced data manipulation. However, we now enter dangerous territory where our apps are heavily dependent on external data and the Apollo library.
Writing tests is essential for any Apollo application that aims to be scalable, robust and allows its developers to sleep soundly at night. By harnessing a range of tools you will be able confidently release, ship higher quality code and improve your team’s efficiency.
This article uses Typescript, Apollo version 3 & React Testing Library for examples.
Unit testing versus Integration testing
Before diving into some tips and tricks it is important to note that the following suggestions apply to integration style testing. Integration testing involves assessing how multiple components in your app work together rather than a single component in isolation. When you are using Apollo in your app it is important that you lean more towards integration testing since the Apollo client itself has a drastic impact on how data is received, manipulated and hence presented in your application.
Do not mock individual functions 🚫
In the example below we have a function fetchCalendar
which interacts with our Apollo client to request a list of dates. This is a common pattern in Apollo where we request some data, handle errors and extract a response.
When we define our client queries and mutations in separate files it can be tempting to to mock these files directly. We could mock the fetchCalendar
behaviour in a test by doing the following.
The issue here is that we are assuming how requests will be handled. As a result, the application's behaviour is not a true reflection of the production system. It is dangerous to create mocks that do not reflect the true nature of what is being performed in our application’s logic. For instance, if we were to change logic in fetchCalendar
such as adding new fields, returning an array instead of a single object or the way we extract the calendars from the response, our mocks would not provide any test coverage on these changes.
Instead, you should aim to mock the lower level APIs that are used in the file and let the rest of your app execute normally. This gives you higher confidence that your surrounding code is working correctly.
Do mock at the service worker level ✅
By mocking requests at the service worker level you enable your application to behave naturally in a testing environment. You spend less time mocking individual functions which means more test coverage for your code.
Creating mocks using a service worker means that your test will make a HTTP request that is then intercepted at a networking layer before it actually gets sent off to the server.
Here is a high level overview from that describes this process:
One thing that really bothers me about mocking things like fetch is that you end up re-implementing your entire backend… everywhere in your tests. — Kent C. Dodds, Stop Mocking Fetch
The mock service worker library enables you to mock API endpoints alongside the rest of your code.
In the above example we mock out the getCalendar request. Whenever our application attempts to fire a GraphQL request in a testing environment for calendars it will be intercepted and return the value we provided.
This approach has numerous benefits:
We don’t have to mock any files, libraries or functions. ie. More of our code gets tested!
We can test the Apollo configuration that could impact the output such as caching or type policies
Our tests are no longer coupled to Apollo — We could swap out our fetching library or use fetch() directly
Do not define another client for tests 🚫
As Apollo evolved it became more powerful and crucial for our front-end applications. The client itself has a range of critical features that we rely on such as caching, type policies, links, request options and even more. We end up adding core business logic and workflows into these Apollo APIs which means we must test them!
When testing our applications we want to ensure that all of these Apollo APIs work correctly with our app’s architecture. Without including the actual cache, links and other configuration in your tests, a bug could easily slip pass and into production.
Below is an example of an Apollo Client that adds a custom cache configuration, links and query policies.
The example below is an incorrect way to test a component that uses this client. 🚫
In this example we have defined an entirely new Apollo Client for our test. Remember, the Apollo Provider is a complex beast. When testing our apps we want to provide the powerful bear, not the baby cub. We should be exporting and using a single client throughout our application and all our tests.
Do not hard code your mocks 🚫
Below is a common example using Apollo’s Mocked Provider API.
In the example above we are coupling our mocks to the implementation of our GraphQL document. When we make a change to our query fields, we must carefully update our mock as well.
The Apollo MockedProvider outlines that “Your test must execute an operation that exactly matches a mock’s shape and variables to receive the associated mocked response”. This means whenever a field gets added or removed we must manually update our mocks. This ends up creating a scenario where our tests fail not because the code is broken but because our mocks are broken.
This can also become an issue for an application that has unpredictable arguments being sent to the GraphQL server. In our case, we want to send a date argument which is based on today’s date. The MockedProvider does not like this since each day our tests run will produce a different date argument. To avoid our tests breaking each day we end up twisting and turning our application to mock the date implementation. The more we do this, the less our tests reflect our actual application.
Hardcoding your mocks also introduces a situation where you are redefining your server’s schema design. You are making an assumption what the returned type is. This in effect implicitly couples your mocks to your server. If your server decides to change the underlying fields or its types, your mocks no longer truly reflect a payload from a server. You can see this in the above example where todos is returning a number in the list, when it should only be returning a list of strings.
Do rely on the GraphQL type system ✅
The true special power behind GraphQL’s type system is its introspection capabilities — Production Ready GraphQL — Marc-André Giroux
GraphQL by nature is heavily oriented around the types that are defined in its design. As engineers, we have access to a plethora of information around the shape of our schema, the available resolvers and everything in between. By harnessing the metadata that is provided in the schema’s introspection we can create reliable and durable mocks that represent the true nature of our backend.
Fundamentally, we don’t want tests to break unless our actual code is broken. We want to spend time assessing the actual user workflows and business logic in our app. We don’t want to spend time hand crafting and fixing broken mocks.
By utilising Typescript, GraphQL code generator and GraphQL mocking tools we can automatically build responses that are based on the actual underlying schema that our server provides.
First we use graphql-codegen package to fetch our GraphQL introspection and store it as a JSON file. This JSON file provides us the shape of our backend schema and can be used to replicate a realistic server experience.
We run graphql-codegen —-config codegen-introspection.yml
to generate the introspection.json file.
Next we utilise the @graphql-tools/mock
and graphql
npm packages to use the introspection to create a dummy front-end server.
It is as easy as that! Now whenever we make a GraphQL request we no longer need to hard code mocks. The request will be intercepted at the service worker level, figure out what fields were requested and return a valid mocked response. This means if we change fields or arguments in our GraphQL request they will automatically get mocked out instead of causing the tests to fail. Isn’t that great?
Do Test Driven Development ✅
Test Driven Development (TDD) simply put is the act of writing your tests before your actual implementation. In Test Driven Development in React with Jest and Enzyme, Bulat suggests that “what TDD does is promote clean and functional code, prompting the developer to consider things like how the component will be integrated, how reusable and how refactor-able it is”.
Using TDD with your front-end GraphQL features is essential since it gives you a better understanding of how the product will work and also helps you identify flaws in the schema design before you pour hours of work into a back-end API.
By utilising the automatic GraphQL responses from server mock above we can quickly dive into writing TDD tests without needing to spend time mocking server responses.
In this example we did not need to spend time hardcoding mocks. This is great as it allows you to quickly start developing code and use a Test Driven Approach to get your tests passing.
This approach is also beneficial for rapid development; you do not need to spin up your application on a development server to simulate the GraphQL server interactions. You can develop entirely inside of your IDE and testing environment.
However, when doing Test Driven Development we often need to test more advanced cases than simply loading and displaying data. A GraphQL response can return endless variations of data. In the below example we explore customising a specific GraphQL response by updating our mocks that we register on our fake server.
Whenever the calendars resolver is called, the component will receive the above list of calendars. It is important to note that any fields we do not explicitly define will be automatically mocked out using a default value.
Now we can go ahead and write some tests to assert if our todo list is functioning correctly.
All that is missing is to go into Calendar.tsx
and start implementing the code so your tests can pass.
In Mocking is a Code Smell, Eric Elliot explains that “TDD tends to have a simplifying effect on code, not a complicating effect. If you find that your code gets harder to read or maintain when you make it more testable, or you have to bloat your code with dependency injection boilerplate, you’re doing TDD wrong.”
By using this test driven approach, you should find that your code becomes easier to test and you spend less time making architectural changes to your code base just to support testing. Your tests will be more decoupled from your implementation, will be less prone to false negatives and will scale as your application grows.
Conclusion
As we move towards a more rich web ecosystem we need to ensure our applications will work when they depend to a great extent on external packages like Apollo and product-driven data sources like GraphQL. By incorporating the range of suggestions outlined in this article, you will be able to spend less time writing tests and can ensure that your application’s architecture and Apollo client will work together in harmony.