DEV Community

Cover image for API Testing with Cypress: Part 2 - Creating your tests
Murillo Welsi de Souza Pereira
Murillo Welsi de Souza Pereira

Posted on • Updated on

API Testing with Cypress: Part 2 - Creating your tests

Hello again guys!!

Moving forward with the subject we discussed in the previous post, let's focus on to the part that is most important to us, the tests.

HTTP Methods

The resources of a microservice based applications can be manipulated in several ways. It is possible to create, update, delete them, and do other operations as well.

When we send a request for a service, we need to inform the URI to identify which resources we intend to handle. We also need to inform the type of manipulation we want to do on the resource. For this, we will use the HTTP protocol methods.

The HTTP protocol has several methods, each one has a different function. The most used are:

  • GET: retrieve data for a resource.
  • POST: create a new resource.
  • PUT: make a change to a specific resource that already exists.
  • PATCH: partially update a given resource.
  • DELETE: delete a particular resource.

Users Endpoint

Back to our target API - ServeRest, let's start by testing the user's endpoint:

Alt Text

Goals

  1. Use the GET method in /usuarios to retrieve all users data.
  2. Validate query params.
  3. Validate status code.
  4. Validate the content of the response body.

So, move your asses. Let's do it!

We will remove all the contents of the previous post from the Integration folder. That’s where our tests should be, so we’ll just leave what’s needed.

You can remove it manually or use the command below if you are lazy like me:

rm -rf cypress/integration/*
Enter fullscreen mode Exit fullscreen mode

Now we are going to create a folder called Usuarios, which we will use to store all requests related to the User's endpoint. This will keep our code organized.

Inside the Usuarios folder, we will create our first test spec, called GETusuarios.spec.js

Now the project structure should look like this:

├── fixtures
├── integration
│   └── Usuarios
│       └── GETUsuarios.spec.js
├── plugins
│   └── index.js
├── support
│   ├── commands.js
│   └── index.js
└── videos
Enter fullscreen mode Exit fullscreen mode

GET

GETUsuarios.spec.js

Let's add the basic Mocha organization structure. You can use BDD style if you want:

  • describe - Given -> Test Suite name
  • context - When -> Test Inner Suite name
  • it - Then -> You should place your tests here!
/// <reference types="cypress" />

describe('Given the Users api', () => {
  context('When I send GET /usuarios', () => {
    it('Then it should return a list with all registered users', () => {
      // place your tests here
    });
  });

  context('When I send GET /usuarios passing id query param', () => {
    it('Then it should return only the filtered user', () => {
      // place your tests here 
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

First Test Case

Now let's use Cypress method cy.request to make the HTTP request:

  • method - GET
  • url - API address + endpoint !
cy.request({ 
   method: 'GET', 
   url: 'https://serverest.dev/usuarios'
})
Enter fullscreen mode Exit fullscreen mode

After that we will call the .should function, which I mentioned in the previous post. It will enable us to make multiple assertions on the yielded subject - response in this case.

.should((response) => {
  // all your assertions should be placed here!!
});
Enter fullscreen mode Exit fullscreen mode

Let's add a log to see what the 'response' is returning in the body:

cy.log(JSON.stringify(response.body))
Enter fullscreen mode Exit fullscreen mode

In short, cy.log will access the property 'body' of the 'response'. The JSON.stringify function will be used to transform the response body into a string.

Alt Text

Run the command cypress:open and check what the log returns to us.

npm run cypress:open
Enter fullscreen mode Exit fullscreen mode

Alt Text

Awesome! We can conclude that our call is working correctly, as we are receiving the response body correctly and 200 status code (success).

Let's remove cy.log (we don't want trash in our tests) and add some status code and response body assertions.

We can validate the easiest one first, the status code:

expect(response.status).to.eq(200)
Enter fullscreen mode Exit fullscreen mode

Cool! The above code means that we expect the status code of the response to be equal to 200.

We can assert that the quantidade (quantity) key will always have the same number of the usuarios (users) array. Lets add this validation:

expect(response.body.quantidade).to.eq(response.body.usuarios.length)
Enter fullscreen mode Exit fullscreen mode

Let's validate that the email from usuarios field should not be null...

Alt Text

Hmm, but usuarios is a list with three elements. So how do we do that?

We need to pass which index from usuarios list we want to access to do this. For now, let's access the first object in the list, passing 'usuarios[0]':

expect(response.body.usuarios[0].email).to.not.be.null
Enter fullscreen mode Exit fullscreen mode

Alt Text

Great, it worked! But what can we ensure that the 'email' key of all objects within the usuarios array is not null?

Maybe duplicate the previous row, just changing the index?

expect(response.body.usuarios[0].email).to.not.be.null
expect(response.body.usuarios[1].email).to.not.be.null
Enter fullscreen mode Exit fullscreen mode

Yeah, it might even work. BUT what if we had a thousand of users inside this array, would we add a thousand lines to our code?

I don't think so.

To do a smarter assertion, we can use Cypress loadash, which offers us the .each() function:

Cypress._.each(response.body.usuarios, (usuario) => {
  expect(usuario.email).to.not.be.null
})
Enter fullscreen mode Exit fullscreen mode

This function work almost like a forEach(), going through the array and making the assertions now in each 'usuario' object from 'usuarios' array.

Alt Text

Let's take the opportunity to add one last validation to usuarios. We expect each object to have all the keys ('nome', 'email', 'password', 'administrador', '_id'):

expect(usuario).to.have.all.keys('nome', 'email', 'password', 'administrador', '_id')
Enter fullscreen mode Exit fullscreen mode

Alt Text

Second Test Case

Moving to the next test case, we will send the same request as before, but this time passing a query string to filter only one user by _id:

Alt Text

Adding a validation to ensure that the name is always correct:

context('When I send GET /usuarios passing id query param', () => {
  it('Then it should return only the filtered user', () => {
    cy.request({
      method: 'GET',
      url: 'https://serverest.dev/usuarios',
      qs: {
        _id: '0uxuPY0cbmQhpEz1'
      }
    })
      .should((response) => {
        expect(response.status).to.eq(200)
        expect(response.body.usuarios[0].nome).to.eq("Fulano da Silva")
      });
  });
});
Enter fullscreen mode Exit fullscreen mode

Alt Text

cypress.json - baseUrl

We are repeating the url parameter in both cy.request(). Add the following lines to your cypress.json file, so that it is not necessary to repeat this information.

Set video as false as well. We don't want to Cypress record it for us.

{
  "baseUrl": "https://serverest.dev",
  "video": false
}
Enter fullscreen mode Exit fullscreen mode

Okay, that was a good start and now our code looks like this:

/// <reference types="cypress" />

describe('Given the Users api', () => {
  context('When I send GET /usuarios', () => {
    it('Then it should return a list with all registered users', () => {
      cy.request({
        method: 'GET',
        url: '/usuarios'
      })
        .should((response) => {
          expect(response.status).to.eq(200)
          expect(response.body.quantidade).to.eq(response.body.usuarios.length)
          Cypress._.each(response.body.usuarios, (usuario) => {
            expect(usuario.email).to.not.be.null
            expect(usuario).to.have.all.keys('nome', 'email', 'password', 'administrador', '_id')
          })
        });
    });
  });

  context('When I send GET /usuarios passing id query param', () => {
    it('Then it should return only the filtered user', () => {
      cy.request({
        method: 'GET',
        url: '/usuarios',
        qs: {
          _id: '0uxuPY0cbmQhpEz1'
        }
      })
        .should((response) => {
          expect(response.status).to.eq(200)
          expect(response.body.usuarios[0].nome).to.eq("Fulano da Silva")
        });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Keep in mind that we are sending two requests to the same endpoint. This will need to be refactored, but we will come back to this case later.

POST

POSTUsuarios.spec.js

Moving on to the next HTTP method, we will now create a new file called POSTUsuarios.spec.js. In this file we will put all tests related to the POST method.

Create the test structure using the Mocha functions, exactly as we did in the GET file. Obviously, make changes to the descriptions according to the scenario describe, context and it.

/// <reference types="cypress" />

describe('Given the Users api', () => {
  context('When I send POST /usuarios', () => {
    it('Then it should create a new user', () => {
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

This time the cy.request() function will be slightly different.

  • The method will be POST.
  • The url will remain the same as /users (no need to add the complete url, just add the resource).
  • In the body param we'll add the necessary information to create a new user. To find out what information is needed, always consult the API documentation at ServeRest.

Alt Text

Our payload will look like this:

body: {
  nome: "Dumb John",
  email: "dumb.john@qa.com.br",
  password: "test",
  administrador: "true"
}
Enter fullscreen mode Exit fullscreen mode

Let's take the opportunity to validate the success message and status code.

Alt Text

/// <reference types="cypress" />

describe('Given the Users api', () => {
  context('When I send POST /usuarios', () => {
    it('Then it should create a new user', () => {
      cy.request({
        method: 'POST',
        url: '/usuarios',
        body: {
          nome: "Dumb Joe",
          email: "dumb.joe@qa.com.br",
          password: "test",
          administrador: "true"
        }
      })
        .should((response) => {
          expect(response.status).eq(201)
          expect(response.body.message).eq("Cadastro realizado com sucesso")
        });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Alt Text

Success! Are you sure? Try to run the test again.

Alt Text

When we run the test again it fails, but why? This is because, according to the business rule, it is not allowed to register a user with an already used email. Let's use a strategy to solve this problem.

Let's edit the file located in plugins > index.js

We will use lib Faker to create a different user for each request. Install faker using the npm command:

npm i -D faker
Enter fullscreen mode Exit fullscreen mode

I named this task as freshUser() and inside it we will pass key/value using faker lib:

freshUser() {
  user = {
    nome: faker.name.firstName(),
    email: faker.internet.email(),
    password: faker.internet.password(),
    administrador: "true"
  };
  return user;
}
Enter fullscreen mode Exit fullscreen mode

The complete file will look like this:

/// <reference types="cypress" />

const faker = require("faker");

/**
 * @type {Cypress.PluginConfig}
 */

module.exports = (on, config) => {
  on("task", {
    freshUser() {
      user = {
        nome: faker.name.firstName(),
        email: faker.internet.email(),
        password: faker.internet.password(),
        administrador: "true"
      };
      return user;
    }
  })
  return config
}
Enter fullscreen mode Exit fullscreen mode

Now refactoring POSTUsuarios.spec.js to usefaker. Create a variable called fakeUser.

let fakeUser;
Enter fullscreen mode Exit fullscreen mode

In order to create a different payload before each execution, we will use a hook called beforeEach(). Within this hook, we will call the function cy.task() throwing user created by the task to the fakeUser variable.

beforeEach(() => {
  cy.task('freshUser').then((user) => {
    fakeUser = user;
    cy.log(JSON.stringify(fakeUser))
  });
});
Enter fullscreen mode Exit fullscreen mode

I also added a cy.log() to see the user being created for each request. REMOVE THIS FROM YOUR CODE.

Change the body parameter to receive fakeUser variable as well.

cy.request({
  method: 'POST',
  url: '/usuarios',
  body: fakeUser
})
Enter fullscreen mode Exit fullscreen mode

The refactored file looks like:

/// <reference types="cypress" />

let fakeUser;

describe('Given the Users api', () => {
  beforeEach(() => {
    cy.task('freshUser').then((user) => {
      fakeUser = user;
      cy.log(JSON.stringify(fakeUser))
    });
  });

  context('When I send POST /usuarios', () => {
    it('Then it should create a new user', () => {
      cy.request({
        method: 'POST',
        url: '/usuarios',
        body: fakeUser
      })
        .should((response) => {
          expect(response.status).eq(201)
          expect(response.body.message).eq("Cadastro realizado com sucesso")
        });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

We can now run the tests as many times as we want.

Alt Text

I will finish this post now, because we already have a lot of information here!

Please keep in mind that just reading this post will not make you learn. Make your own code, practice, fail and fail again until you succeed!

Check this project repository on Github

GitHub logo murillowelsi / cypress-api-tutorial

Repository created in the article REST API Tests with Cypress

cypress-api-tutorial

Repository created in the article REST API Tests with Cypress

Feel free to add me on LinkendIn. Leave your comments, questions and suggestions.

Thanks for your attention, see you on next post!!

Discussion (9)

Collapse
just_papu profile image
Rogerin

This article is amazing, thank you it will help me a lot on my api testing project. ❤

Collapse
cezarmzz profile image
Cezar Augusto Mezzalira

Hey Murillo!
I applied their knowledge to a project and really liked the result.
Thanks for your post series about cypress.

Collapse
murillowelsi profile image
Murillo Welsi de Souza Pereira Author

Really Cezar?
That's awesome, dude. Thank you for the feedback!

Collapse
cezarmzz profile image
Cezar Augusto Mezzalira

Yeah man!
But in my case, Cypress don't make so much sense, because I need to get unit tests too.
In a cenario without any tests and the only way to test is by making integration tests, Cypress fits well 😍
Thank you for tour reply!

Collapse
chandrasekhar_reddy_281e8 profile image
chandrasekhar reddy • Edited on

Hi I have a question . in my project I am getting a html response where I need to get the data from that response and compare it with the list I have sent .
I am new to API, so can you give me a example how to get response code from html

Collapse
mathiar42778659 profile image
mathi arasan • Edited on

Hello Murillo
When I use cy.task , its giving an error. Unable to resolve. Please help.
The 'task' event has not been registered in the plugins file. You must register it before using cy.task()

Fix this in your plugins file here:

Collapse
asn007_17 profile image
asn-007

Thanks Murillo for this great post. This was an inspiration for me to write the first API test using Cypress.
One of the challenges I am facing, if you could help me with is I have a fixture file(json) which I use to send API Requests and also validate the responses.
But when one of the "expect" assertion fails, the remainder of the assertions are skipped AND it also doesn't read the 2nd test data from the fixture file.
How can I fix this so that even if one of the first fixture data fails, I am able to continue with the rest of the assertions in the 1st fixture data and also be able to go to the next test data entries(2nd, 3rd...nth) in the fixture file.

Collapse
asn007_17 profile image
asn-007

To be more specific I would want to be able to continue test execution with the rest of the data from the fixture file when an assertion fails with one of the test data.

Ex: I have this json file as fixture,
when I assert "email" from the first fixture data which is invalid cypress throws an error and stops execution.

But I would like it to continue with the assertion for age and also the data from 2nd and 3rd fixture data and not just stop the entire execution at the 1st failure
[
{
"name": "valid1",
"email": "invalid1",
"age" : valid
},
{
"name": "valid2",
"email": "valid2",
"age" : valid
},
{
"name": "valid3",
"email": "valid3",
"age" : valid
}

]

Collapse
karla_mieses_8cdf9f6989ad profile image
Karla Mieses

Thank you for your post Murillo, it really helps, well done!