DEV Community

Marina Mosti
Marina Mosti

Posted on

Loading and using a fixture from an API endpoint for Cypress e2e tests

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 (2)

Collapse
 
jprealini profile image
Juan Pablo

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?