DEV Community

loading...
Cover image for API Testing with Cypress: Part 2 - Creating your tests

API Testing with Cypress: Part 2 - Creating your tests

Murillo Welsi de Souza Pereira
I’m a QA Engineer with five years experience in software testing. Certified Tester - ISTQB - CTFL.
・8 min read

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