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:
Goals
- Use the GET method in
/usuarios
to retrieve all users data. - Validate query params.
- Validate status code.
- 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/*
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
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
});
});
});
First Test Case
Now let's use Cypress method cy.request
to make the HTTP request:
- method - GET
- url - API address + endpoint ! ```javascript
cy.request({
method: 'GET',
url: 'https://serverest.dev/usuarios'
})
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.
```javascript
.should((response) => {
// all your assertions should be placed here!!
});
Let's add a log to see what the 'response' is returning in the body:
cy.log(JSON.stringify(response.body))
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.
Run the command cypress:open
and check what the log returns to us.
npm run cypress:open
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)
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)
Let's validate that the email
from usuarios
field should not be null...
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
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
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
})
This function work almost like a forEach(), going through the array and making the assertions now in each 'usuario' object from 'usuarios' array.
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')
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
:
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")
});
});
});
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
}
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")
});
});
});
});
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', () => {
});
});
});
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.
Our payload will look like this:
body: {
nome: "Dumb John",
email: "dumb.john@qa.com.br",
password: "test",
administrador: "true"
}
Let's take the opportunity to validate the success message and status code.
/// <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")
});
});
});
});
Success! Are you sure? Try to run the test again.
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
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;
}
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
}
Now refactoring POSTUsuarios.spec.js
to usefaker
. Create a variable called fakeUser
.
let fakeUser;
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))
});
});
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
})
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")
});
});
});
});
We can now run the tests as many times as we want.
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
Feel free to add me on LinkendIn. Leave your comments, questions and suggestions.
Thanks for your attention, see you on next post!!
Top comments (9)
This article is amazing, thank you it will help me a lot on my api testing project. ❤
Hey Murillo!
I applied their knowledge to a project and really liked the result.
Thanks for your post series about cypress.
Really Cezar?
That's awesome, dude. Thank you for the feedback!
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!
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
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:
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.
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
}
]
Thank you for your post Murillo, it really helps, well done!