Mocking HTTP Calls in Cypress End-to-End Tests

Don’t let unreliable test data keep your new features from making it to production

Paige Niedringhaus
Bits and Pieces

--

Photo by UX Indonesia on Unsplash

Introduction

If your web development team is anything like mine, you’ll understand the value and importance of tests to support and continue to verify your application’s functionality (even if you’re not a huge fan of writing tests, which sometimes, I’m not).

Unit tests, integration tests, and end-to-end tests in particular are a way to safeguard against bugs popping up in seemingly unrelated pieces of code while you’re building out cool, new features.

End-to-end testing, in case you’re less familiar with it, is a technique for testing the entire software product from beginning to end to ensure the application flow behaves as expected. Basically, an automated test is written to control a browser as a user would; it types text into inputs, clicks buttons, looks for page elements, etc. just as if a user was controlling the application, to ensure the system behaves as intended.

But if your web application is anything like the one we build and support, it depends on external data sources from other teams, both within and outside of our organization, which means we can’t always rely on that data (especially when it comes to non-production data) to be clean, accurate, or — in some cases, even in existence. This is why, when it comes to writing and running our end-to-end (e2e) tests (which are the tests that depend on external data), we mock (or “stub”) those API calls instead of hoping the stars align and no one else has changed that same data to suit their needs.

Unreliable lower life cycle data, outside of the control of your development team, is no reason to keep new code from being merged in and deployed to production. But the old adage “It works on my machine” 🤷‍♀ is also not a valid justification because who knows that the same will hold true when deployed somewhere besides a local development laptop.

So let’s discuss how to cut out the (potentially) bad data and stub good data (the kind of data scenarios we’d expect to encounter in real life) when using Cypress for end-to-end testing.

Tip: Use Bit (Github) to share, document, and manage reusable React components from different projects. It’s a great way to increase code reuse, speed up development, and build apps that scale.

Meet the Key Player for e2e Tests

For our React application, we use the popular e2e framework Cypress to run our own end-to-end tests, and one awesome feature of Cypress is that it’s actually JavaScript framework agnostic and can be utilized even if you’re writing an app in vanilla JS.

This is great news, because it means this tutorial can apply to you and your app, even if you’re using Angular, Vue, Svelte or some other cutting edge JS framework I’ve not even heard of yet. 😄

So Why Cypress?

When we started building our current application we actually began building our e2e tests with Puppeteer, and it was never much fun. The syntax was unintuitive, the tests were flaky, debugging was hard and it just wasn’t a great developer experience overall.

I’d heard Cypress talked about by many developers I really respected, and without too much convincing on my part, my team gave it a try and we’ve been using it ever since. Cypress just makes end-to-end tests easy in a way I’ve never experienced before (I’ve also worked with Selenium in the past, and that too had a bit of a steep learning curve), set up was a breeze, debugging is much nicer with a great headed browser feature, and the documentation on its website is pretty good, to boot. And Cypress can work with any browser-based JavaScript application — as I mentioned above.

So if you’re considering different e2e testing frameworks and Cypress is an option, I would recommend you give it a try. I doubt you’ll regret it.

So now that I’ve sold you on why we chose Cypress for our e2e testing, it’s time to describe how to mock data requests so malformed (or missing) data outside of your control doesn’t bring your development to a screeching halt.

Setting up Mocked Data for Cypress to Return when Queried

I’m going to be demoing how to do this test mocking with a shopping cart: displaying an empty cart, displaying a list items to purchase, and displaying a list of cart items already purchased. Bear in mind the examples are simplified for ease of understanding, but the parts important to successfully mocking these HTTP calls with different data is there.

The first step in this tutorial revolves around stubbing the right data that should be returned when Cypress queries a REST endpoint.

That’s easy enough to do. In our project there’s a cypress folder at the root level of the project that holds: all the Cypress tests (in the /integrations folder), the mocked routes (in the /mocks folder), and the folder of test JSON data (in the /fixtures folder).

We’ll begin in the /fixtures folder. It’s here that I’ll create a JSON file of all the test data I need, and name it cart_mock_data.json.

cart_mock_data.json

This JSON file contains the stubbed cart data to be returned by the mocked HTTP call.

This file contains three different data sets, which will be returned by the same mocked HTTP route according to which root data object is called: getCurrentCartItems, getEmptyCart or getPreviouslyOrderedItems.

Don’t worry if that sentence seems a little unclear, it will make more sense when you see it implemented in practice.

As you can probably guess, the current cart items are items still to be bought, the empty cart data is what a cart call with no cart items looks like, and the previously ordered items are items bought and in various states of being packed, shipped and delivered to the customer.

Now that the data is stubbed out, let’s move on to mocking the HTTP call that will deliver this data during the tests.

Writing the Mocked HTTP Calls

Cypress makes setting up managed network requests very easy with cy.route(). This method is flexible and can take in a variety of parameters, and the way we use it is with the syntax of: cy.route(method, url, response).

These route mocking files go in the /mocks folder (seems logical, right?), and this one, since it’s related to the shopping cart is named: cartMocks.js.

cartMocks.js

The code to mock the HTTP call for the Cypress end-to-end cart test.

To make the route mocks more flexible and reusable, we made each real HTTP call in our application into a separate arrow function mocking that same call.

And as you can see from the code snippet above for the function getCartItems(), it defines an endpoint (and is slightly more reusable because the endpoint can be modified to fetch already purchased cart items too, if the type parameter is "purchased"), and then sets up the mocked route. The method is a "GET", the url consists of the endpoint variable plus the user’s userInfo.userName to ensure the right cart items for the user are fetched, and the response is whatever we want it to be (which is defined in the cart_mock_data.json file).

The as('getCartItems'); code at the end of the cy.route() call is an alias in Cypress. When using an alias with routes in Cypress, it’s an easy way to ensure your application makes the intended requests and waits for your server to send the response. You’ll see an example of route aliases in action in the actual tests below.

Replacing Actual HTTP Calls with the Mocked Calls in Cypress Tests

And the last step in the tutorial: how to set these HTTP calls up so they return the stubbed data instead of attempting to hit your actual databases or backend services.

The actual test files go in the /integrations folder, and since this one pertains to the cart, it’s named cartSpec.js. Below, I’ll show you three separate tests (which can all be in the same test file, just different describe() test blocks), using the same mocked route and returning different data each time.

First up is the test where the cart should show as empty.

cartSpec.js

This test checks that the cart has an empty icon when there’s no cart items returned from the mocked endpoint.

Let’s step through each piece of this test.

Before you can begin routing requests, you must start the server, hence the cy.server() you’ll see in each test’s beforeEach() function that sets up the environment for the test.

Next, the cy.fixture() function is how to actually load a fixed set of data located in a file (the cart_mock_data.json file, for us) by providing the file path as the function param, and then returning the promise it creates to access that fixture’s data and pass it to our mocked route.

The rc is the data inside of the file, and by passing rc.getEmptyCart to the cartMocks.getCartItems() mocked route (which is imported at the very top of the file) plus a userName object and "pending" cart item type, when that data in the JSON file is accessed, the object with the key of getEmptyCart will be returned.

Then the cy.visit() navigates the browser to the cart page by passing the in the /cartPage URL.

And finally, the route alias I referenced earlier above comes into play with cy.wait('@getCartItems'). The cy.wait() actually waits for a specified number of milliseconds OR for an aliased resource to resolve before moving on to the next command. So when this beforeEach() function runs, it requests the mocked data and ensures it’s returned before moving on to the actual test.

In this case, it’s easy because the returned data is an empty array [], and the test just verifies the text "Your Cart is empty." is present on the screen. Not too tough, right?

Here’s another test (in the same test file), but this one tests whether cart items that have yet to be purchased appear in the cart when data is supplied to the getCartItems mock.

cartSpec.js

This test checks that the cart displays cart items not yet purchased when pending cart items are requested from the mocked endpoint.

This test should be more self explanatory after the thorough run down I gave in the first test.

Once again, set up the mock server and fixture to return data from the JSON file. You will notice, however, for this test, the getCurrentCartItems object is returned this time when the userName and "pending" cart item type are supplied to the HTTP mock call. This data is a list of cart items that resemble actual cart item data that comes back from our actual cart service, minus having to depend on having a real user’s cart at hand with cart items present.

And in the actual test, inside the it() statement, the end-to-end test checks to make sure the “test product” cart item names are displayed in the correct order, with most recently added items on top.

Seems pretty easy again, right? Let’s look at one more test, this one will test that only purchased items are displayed when the "purchased" cart item type parameter is passed into the mocked HTTP call.

cartSpec.js

This last test checks that the cart displays already purchased cart items when purchased cart items are requested from the mocked endpoint.

For this test, the same steps happen again: start the mocked server, call the fixture and pass it JSON data, specify the getPreviouslyOrderedCartItems response object as well as a userName and "purchased" cart item type for this call (since that endpoint is modified slightly from the original getCartItems URL), and visit the cart item page, waiting for the mocked HTTP call to return data.

Then the test checks that just already purchased cart items are present, and we’re done.

Of course, Cypress e2e tests can (and do) get a lot more complicated than the examples I’ve shown above, but hopefully this will help get you going in the right direction with your own e2es, especially when mocked data from another server or service is needed.

Conclusion

Testing, although it may not be fun to write, helps ensure that all parts of a web application continue to function as they should, even as new features are added. End-to-end tests, one of the myriad of testing types available tests an application’s flow as an actual user would: toggling buttons, clicking drop downs, checking items are visible or not on a page, etc.

But although tests (and good test coverage) are a critical part of my development team’s process, the data we depend on from other teams, outside of our control, is not always the most reliable. For this reason, we choose to mock our Cypress HTTP calls to those outside services in our e2es to ensure that bad test data isn’t the thing holding us back from adding new functionality our users want. And trust me, it’s saved us a ton of extra time and headaches trying to debug why things aren’t working right.

Check back in a few weeks — I’ll be writing more about JavaScript, React, ES6, or something else related to web development.

Thanks for reading. I hope this helps you write end-to-end tests with more ease, giving yourself and your team more confidence that all parts of your system work as expected, despite adding new code and features, or having unreliable test data in lower life cycles. 🙂

--

--

Staff Software Engineer at Blues, previously a digital marketer. Technical writer & speaker. Co-host of Front-end Fire & LogRocket podcasts