I love Cypress, it’s one of those tools that for me has just made e2e testing actually fun and engaging. In this article, we’re going to explore a common scenario that you may encounter when doing e2e testing on your apps: fetching a data fixture
from an API endpoint before your tests and using it to write your tests.
What is a Cypress fixture?
In case you haven’t heard of fixtures before, you can think of them as a predefined piece of data that you use in your tests to perform certain actions.
Consider the following fixture example.
{
"username": "yoda",
"password": "secureiam",
"id": 123
}
If we have an endpoint in our application that for example holds the user’s settings page, we may need to construct the following URL:
http://www.holocrons.com/123/settings
In this demo URL, the first segment is the id
of the user, so based on our dummy fixture it would be 123
.
In Cypress, to navigate to a URL you use the visit
command, so you may want to construct your action as follows:
cy.visit('/123/settings')
The problem here however is that we are hardcoding a user id into our tests. In most cases this will become a problem, because unless our backend and database are set up to work with a specific id, if that user ceases to exist for some reason then all of our e2e tests will break.
The first solution is to create a local fixture, so you would go into your cypress folder structure, and create a user.json
file inside the appointed fixtures
folder. Inside, we would paste the content of our demo fixture.
# user.json
{
"username": "yoda",
"password": "secureiam",
"id": 123
}
Now, in order to use this fixture inside our test, we could set up a beforeEach
hook inside our spec file, and load it as follows.
describe('my tests', () => {
beforeEach(function() {
cy.fixture('path/to/user.json').as('userData').then(() => {
// Do some things like setting up routes.
});
})
})
The Cypress fixture
method takes a string param that points to where the fixture
file is located. This path is based off where your root cypress.json
file is located.
Once the file is loaded into the fixture, you can use then
block to trigger some extra actions like setting up your cy.server()
and some routes.
Notice that in this example we’re setting up an alias with .as(
'
userData
'
)
. This is important if you’re going to be using the loaded fixture data later on in your tests, outside of the then
block.
Let’s now take our new fixture userData
and actually use it to set up some routes. Because we are inside the then
block you have two choices, you can receive as the param to the method the newly loaded fixture
, or you can use this
to access it as a property.
In these examples we’re going to use the this
approach, but be warned - in order for this
to keep the correct context, we have to use regular function ()
syntax, not arrow functions on our it
calls! If you don’t do this, you will get undefined
errors due to how arrow functions this
context works.
Let’s create a router
alias for our settings page.
describe('my tests', () => {
beforeEach(function() {
cy.fixture('path/to/user.json').as('userData').then(() => {
// Do some things like setting up routes.
cy.server()
cy.route(`${this.userData.id}/settings`).as('userSettings')
});
})
})
Notice how in this case we are using this.userData.id
to explicitly declare that our route
needs to point to user id 123
, the one that was declared in the fixture. Now we can access any data inside our user.json file through the this.userData
property.
Here’s another example inside an it
block.
it('shows the users username in the header', function() {
cy.visit(`${this.userData.id}/settings`)
cy.contains('h1', this.userData.username)
})
Our tests are now hard-code free. But what happens when our tests are driven by some sort of function or endpoint that populates our database with dummy data? We are now going to look at the same problem/solution, but by loading the fixture
from an API endpoint.
Loading API based fixtures
In order to get our fixture from an API, we are going to create some Cypress custom commands to make the code easier to use in our tests.
Head over to your commands.js
file, and let’s get started.
Cypress.Commands.add('loadFixture', (savePath) => {
cy.request({
method: 'post',
url: `api/path/to/fixture/endpoint`,
timeout: 50000
}).then(res => {
cy.log('Fixture loaded from API');
cy.writeFile(savePath, res.body);
cy.log('Fixture written to disk');
});
});
Let’s take a closer look at the code. First we create a new Cypress command and call it loadFixuture
, it will receive a single param called savePath
. This will be a string, and the path to where our fixture will be saved in the disk.
Inside our command, we first call cy.request
to make a network request to our fixture endpoint in the API. This endpoint should give us a JSON response, so make sure to adjust the url
paramter to match your app’s needs - you can also of course pass paramters in the body or query string as needed. Check out the documentation for cy.request
for more options.
After the request completes, we chain a then
callback that gives us the result from our request - this res
holds the response from the API.
We make a couple of cy.log
statements so that we can track from our Cypress log what is happening, and finally call a cy.writeFile
command with the savePath
that we passed to our command, and the body
from the network response. This will write the JSON to a file.
You may be wondering at this point why we are writing the network result to a file, we need this information to be in a file so that we can later on read it using the cy.fixture
command.
With our new command, we can now update our tests to use the new loading.
describe('my tests', () => {
before(function() {
cy.loadFixture('path/to/user.json')
})
beforeEach(function() {
cy.fixture('path/to/user.json').as('userData').then(() => {
// Do some things like setting up routes.
});
})
})
Notice that we are now downloading the fixture on the before
hook, that way we can guarantee that it is only downloaded once since this type of request can usually be heavy on the server and database.
Now, whenever we call the beforeEach
as we did before we will actually be targeting a file that has been downloaded and written before our tests begin, this also guarantees that if necessary you will be working with new generated data.
As always, thanks for reading and share with me your comments on twitter at: @marinamosti
PS. All hail the magical avocado 🥑
PSS. ❤️🔥🐶☠️
Top comments (1)
I am modifying the content of a fixture before sending a POST request, but the item I create with the POST gets persisted with the original data...
I am doing it like:
cy.fixture('fixture').then((fixture) => {
fixture.body.title = "New title",
cy.request({
method: 'POST'
url: 'url',
body: fixture
})
})
Any idea what I'm missing?