Dynamic XHR responses recording & stubbing with Cypress

Cypress is an amazing tests runner for end-to-end testing. Here at AX2, we’re in the process of integrating it in our workflow, along with Jest that we use for unit testing.

One of the things we liked most about Cypress at first glance was how easy it is to get started with, whereas other e2e tests runners out there might be a bit intimidating because they require a more complex configuration before being functional. To start testing with Cypress, you just need to install the package, and you’re good to go: Cypress’ binary will be installed and the basic files structure that Cypress requires will be created in your project.

One powerful feature in Cypress is the ability to stub XHR responses. This means that when your app fetches data from an API, you can intercept that request and let Cypress respond to it with local data from a JSON file. This is very useful to keep consistency from one test to the other.

You don’t necessarily want to use this feature because not stubbing XHR responses also lets you test your API, which is closer to a real user experience. But if your focus is on testing the app itself and you don’t want to deal with potential issues API-side (i.e. the API is down or responds slowly) then this is a great option. Read more about stubbing the backend here.

So basically, if you wanted to do a simple XHR stub, you would do something like this:

describe('MyApp', () => { it('Works', () => {

// Start Cypress' server to hook into XHR requests cy.server()

// Override calls to URLs starting with activities/ and use the // content of activities.json as the response cy.route('GET', 'activities/*', 'fixture:activities.json')});

Pretty simple! All you have to do is start Cypress’ server by calling cy.server() and define some routes to be intercepted and answered with JSON from a local file.

But what if your app makes dozens of XHR calls, each with their specific parameters and responses? You would have to manually download your API’s responses and define a route for each of them in your tests. And if you were to change the way URLs are formatted, or if the data from the API changes, you would need to update all these fixtures by hand… pretty laborious.

We were in that exact situation and we thought we needed a way to perform a “record run” of our tests, where the API would be called for real and we would save every response locally in order to use those as fixtures for subsequent tests.

Thankfully, Cypress has all you need to do that:

cy.writeFile() lets you write data to a file anywhere in the project’s directory

Note how we used the RECORD env variable to define behaviours specific to the record mode. With the code above, any GET request that our app performs is passed through Cypress’ server where we can catch the response and append it to an array that we declare before the actual tests. For each request, we also keep the full URL, which we’ll use later when defining our routes; we also keep the request’s method, which might prove useful if we wanted to stub other requests than GETs.

Now to actually store the data locally, we hook into after() and call cy.writeFile() from there:

Running cy:record should result in a fixture.json file being created in the fixtures/ directory.

Example of a “record run”, notice the “WRITEFILE” log at the end

You might be tempted to write XHR responses in Cypress’ server onResponse() hook, but this would result in Cypress returning an error: “Cypress detected that you returned a promise from a command while also invoking one or more cy commands in that promise.”Writing the data in the after() hook circumvents this problem (and is probably a better option anyway).

All that’s left to do is to load our fixtures when running the tests in normal mode:

Conclusion

If you did something similar or if you have ideas on how this could be improved, please share your thoughts with us!

Also, it looks like Cypress’ team is working on the Plugins API. It would be interesting to see if it evolves to a point where all this recording & stubbing process could be bundled as a plugin to make it even easier to implement this behaviour in test scenarios.