Want to increase your quality and reduce your deployment times? Meet App-level Integration Testing
By: Aislinn Hayes
Tags: Mock Service Worker, Quality, Continuous Delivery, Deployment, React Testing Library

There’s a lot of stuff mentioned there, with one very special and important tool being PACT testing (to help us tackle those server-side issues) that Brendan Donegan spoke about at a recent TestHeads Dublin meetup, and Francislâiny Campos covered in a previous blog post. For now, let’s focus on Integration Tests.

What is App-level integration testing?

First of all, application-level integration tests don’t run in the browser. They run locally when developers make changes, and in our continuous integration pipelines, at the same time as our unit tests. They mount the entire application as part of the test, and mock only the minimum amount of functionality necessary to get the app to perform like it would in a browser.

This will usually mean mocking any API calls that the app makes, and not much more. We do this to adhere to the guiding principles of the test framework we use — React Testing Library, which state:

The more your tests resemble the way your software is used, the more confidence they can give you.

In a nutshell, you want to load your whole application in the test and perform interactions in the same way as a user would. This means clicking on buttons and entering text in input fields — not calling functions directly or mocking pieces of the underlying UI.

What’s wrong with mocking lots of stuff?

Let’s be clear, outside of the context of app-level tests, nothing! If you’re at all familiar with unit testing or writing local tests, you might already know how painful it can be to test things in isolation. Mocking things makes it easier to avoid complexity unrelated to your specific scenario, and often your unit test files can ends up with a lot of jest.mock(... functions at the start for all sorts of inner components and files. You might also be familiar with ways to mock app state — wrapping inner components in all sorts of test utilities and providers that your App.js file would normally supply that let you mock an imaginary state for different scenarios.

We don’t want to do that here though. The point of app-level testing is to try and replicate some of the scenarios you might have traditionally tested in an e2e suite in the browser — and to do that, you need to get your app looking as close to “real” as possible. For example, would an e2e test, or a user for that matter, ever look at the scores page where the header component is mocked and just shows a random <div>Mock header</div> ? Absolutely not. So we don’t do it in our integration tests!

What can be tested in app-level integration tests?

It’s worth noting that as per our pyramid, our expectation of what tests we ship with each feature has shifted somewhat. Instead of writing e2es for every single bug and feature, we expect that the majority of testing will be performed at the application integration level, and the e2e suites are reserved for high level user flows that might traverse many different applications or parts of Ed.

So, when we ask ourselves at the outset of a feature or story what testing we need, a better way to phrase this question is to describe what can’t be tested in integration tests!

A full user flow that traverses through multiple apps (e.g., a teacher creates an assignment, a student completes the assignment, and a teacher views the results in reporting). This kind of scenario needs an e2e test, both from a technical point of view (as you can only mount one app at a time in an integration test) and from the point of view of the value a test like that gives us.

Any scenario that, if it broke, would cause a major incident for our customers. That assignments scenario above could potentially cause an MI if any of the individual parts broke. I’m also thinking of things here like login, and accessing educational content — those definitely qualify for being covered by e2e tests.

But there are plenty of features that live under those flows that can be very easily tested in integration tests. Different flavours of the assignments pages for example (with pagination / filtering / sorting / different statuses), or what we show to users if and when we ever have network failures.

By writing tests at this level, you will have more confidence your code is correct and overall have fewer tests to write, as you will only need to write the few unit tests which are needed to cover edge cases in your code.

Alright, enough talk! Show me an example!

I’m getting there I promise! Here’s what you’ll need.

What frameworks should I use?

We use following frameworks for our app integration tests:

@testing-library/react — We use this to mount our apps and interact with them. Sarah Flanagan gave a great example on how we use this already in our unit and component testing in her blog post Testing React apps: an introduction to React Testing Library. The test utilities it provides for us involve querying the DOM for nodes in a way that is similar to how a user would find elements on a page (like input fields).

Mock Service Worker — This is what we use for mocking API calls, and lets you define API responses as if you have an actual server running beside your tests — like what should be returned by a GET request to /studentAssignments/:studentId . It simplifies the way we mock service endpoints in our tests so that we don’t need to know anything about how the endpoint is called from the UI (whether it be by something like axios or fetch) we just need to know what the request is and what the response should be.

You may have used Apollo Mocking Provider or even plain old jest to mock the components that make data call-outs in the past — however, we highly recommend Mock Service Worker . Why? Because it’s easier to write, more readable, and goes hand in hand with @testing-library/react .

Imagine, for example, that in your functional UI code, you want to swap out your API-calling framework from Apollo to something else, or you decide to rename the module you were using in your jest.mocks . If you do that, your tests will also need to be updated to reflect the new implementation if you use Apollo Mocking Provider or jest.mocks . However, if you use Mock Service Worker to mock the endpoints, your tests will be decoupled from the implementation detail of your code, and will only need to be updated if the actual endpoint you call changes!

Got it — let’s dive in.

Say you have a React app that does something fairly simple, like one that shows a button you can click to fetch some data. After the user clicks the button, a loader icon will appear while the GET request is fetching some data (https://jsonplaceholder.typicode.com/ is a useful free site for this), and then it displays the data on screen.

I used create-react-app to whip up some quick and dirty code for this if you want to follow along. Make sure you have Node >= 10.16 and npm >= 5.6 locally, navigate to a new directory in your terminal, and run the following in a new directory:

$ npx create-react-app app-integration-testing

$ cd app-integration-testing

$ yarn add axios

$ yarn add msw --dev

$ yarn start

Open up your App.js file and update it with the following markup to add the button and some error handling. (For the purposes of readability here and brevity, completely disregard the fact that I’ve crammed a bunch of code into the top level file instead of breaking it out into reusable pieces, or doing any sort of styling, internationalisation, responsiveness, or accessibility testing. I’m feeling reckless!)

Next up, let’s create a reusable helper file here to get Mock Service Worker running in our tests to intercept any API requests that our app might make.

Now, update your App.test.js file to render the app, fire up Mock Service Worker, and interact with it like a user would — clicking the button and checking if the table renders on the page.

For this super simple app, and indeed any app at all, that’s all you need to get started — start Mock Service Worker, render the app, and write out the assertions you expect for a successful interaction. Once you run this test locally with yarn test (or yarn test — watchAll=false , if you want to avoid running the tests in watch mode) the magic of MSW will start to complain at you in the console about any endpoints being called in your test that aren’t mocked.

Look at that! Your test is basically telling you exactly what to do — it’s picked up that your app is making a GET request to https://jsonplaceholder.typicode.com/ that you haven’t mocked a response for. React Testing Library is also very helpfully rendering the DOM when your queries fail to find the table elements on the page, showing you exactly what’s happening to the app in your test.

Now that you know what endpoints you need to mock, you can work backwards — do the scenario you want to automate manually in the browser, and check what responses your API endpoints are providing. I decided to mock a response with just one result, and to make things easier for everyone to understand what the test is testing, I’ve renamed the test file too to DisplaysButtonAndPosts.integration.test.js .

I can also add a test for network failure in almost exactly the same way with another test called DisplaysFailureMessage.integration.test.js .

Awesome, our tests pass! Now what?

That’s it! Now that you’re up and running, the possibilities are endless:

If you have a more complex app with lots of different API requests, you just need to define more handlers for those endpoints and pass them into your getAndSetupServer method. At HMH we generally put those in something like a mockServiceWorkerHandlers.data.js file so that we can reuse them across tests (unit tests too!) in an app.

method. At HMH we generally put those in something like a file so that we can reuse them across tests (unit tests too!) in an app. You can mock all sorts of data scenarios now very easily — imagine you use this app with something like a student scores endpoint and you’ve implemented pagination on the table. You can now test this with another TableWithPagination.integration.test.js file rather than having to set up a user with lots of data and worry about the integrity of that data and the user in a real environment.

file rather than having to set up a user with lots of data and worry about the integrity of that data and the user in a real environment. Our tests are now completely dissociated from the actual real-life services, which, as we mentioned at the start, often have their own deployments / performance issues that can impact unrelated UI testing.

If you ever want to swap our axios for a different API mechanism, you can do so without having to update the mocks in your test — and your tests will scream at you in the console if anything goes wrong. A much quicker feedback loop when you’re caffeinated and in the zone than having to stop and lock up your machine with browser e2e tests.

for a different API mechanism, you can do so without having to update the mocks in your test — and your tests will scream at you in the console if anything goes wrong. A much quicker feedback loop when you’re caffeinated and in the zone than having to stop and lock up your machine with browser e2e tests. You can write tests for all your network failure scenarios to make sure your users always have a great experience, even when your services go down — something that is very difficult to achieve in regular e2es.

And the best part? If you’ve already got e2e tests that are testing things that could be achieved with this pattern, you can delete them and rewrite them as app-level integration tests, reducing the scope of your e2es and speeding up every single e2e test run across every single environment on every test run! 🏃‍♀️