One of the ways to test our applications is using tools like Insomnia, Postman or even through Swagger. However, this entire process is time consuming, we do not always test our entire application whenever we make any changes to our Api.
This is one of the many reasons why automated testing is ideal. So I decided to write this article to present you with a simple and easy-to-understand example, which has a process very similar to what you would be used to (with Insomnia, etc).
So we're going to use two libraries that I love, a testing framework called Jest and the other is a library for doing http testing, called supertest.
And with that we are going to apply a concept called Behavioral Testing, that is, the tests we are going to perform will not have knowledge of the internal structure of our Api, everything we are going to test has to do with the input and output of the data.
The idea of today's application is to add emails to a database (actually it's an array of data that it is in memory), which already has a complete CRUD. All we need to do is test the behavior of those same endpoints.
Let's code
We will need to install the following dependencies:
npm i express
# dev dependencies
npm i -D jest supertest
Now let's pretend that our app.js
looks like this:
const express = require("express");
const app = express();
app.use(express.json());
const fakeDB = [
{
id: Math.floor(Math.random() * 100),
email: "test@example.com",
},
];
app.get("/", (req, res) => {
return res.status(200).json({ data: fakeDB });
});
app.post("/send", (req, res) => {
fakeDB.push({
id: Math.floor(Math.random() * 100),
email: req.body.email,
});
return res.status(201).json({ data: fakeDB });
});
app.put("/update/:id", (req, res) => {
const obj = fakeDB.find((el) => el.id === Number(req.params.id));
obj.email = req.body.email;
return res.status(200).json({ data: fakeDB });
});
app.delete("/destroy/:id", (req, res) => {
const i = fakeDB.findIndex((el) => el.id === Number(req.params.id));
fakeDB.splice(i, 1);
return res.status(200).json({ data: fakeDB });
});
module.exports = app;
And that in our main.js
is the following:
const app = require("./app");
const start = (port) => {
try {
app.listen(port, () => {
console.log(`Api running at http://localhost:${port}`);
});
} catch (err) {
console.error(err);
process.exit();
}
};
start(3333);
Now that we have our Api, we can start working on testing our application. Now in our package.json
, in the scripts property, let's change the value of the test property. For the following:
"scripts": {
"start": "node main",
"test": "jest"
},
This is because we want Jest to run our application tests. So we can already create a file called app.test.js
, where we will perform all the tests we have in our app.js
module.
First we will import the supertest and then our app.js
module.
const request = require("supertest");
const app = require("./app");
// More things come after this
Before we start doing our tests, I'm going to give a brief introduction to two functions of Jest that are fundamental.
The first function is describe()
, which groups together a set of individual tests related to it.
And the second is test()
or it()
(both do the same, but to be more intuitive in this example I'm going to use test()
), which performs an individual test.
First let's create our test group, giving it the name of Test example.
const request = require("supertest");
const app = require("./app");
describe("Test example", () => {
// More things come here
});
Now we can focus on verifying that when we access the main route ("/"
) using the GET method, we get the data that is stored in our database. First let's create our individual test, giving it the name GET /
.
describe("Test example", () => {
test("GET /", (done) => {
// Logic goes here
});
// More things come here
});
Now we can start using supertest and one of the things I start by saying is super intuitive. This is because we can make a chain of the process.
First we have to pass our app.js
module in to be able to make a request, then we define the route, what is the content type of the response and the status code.
describe("Test example", () => {
test("GET /", (done) => {
request(app)
.get("/")
.expect("Content-Type", /json/)
.expect(200)
// More logic goes here
});
// More things come here
});
Now we can start looking at the data coming from the response body. In this case we know that we are going to receive an array of data with a length of 1
and that the email of the first and only element is test@example.com
.
describe("Test example", () => {
test("GET /", (done) => {
request(app)
.get("/")
.expect("Content-Type", /json/)
.expect(200)
.expect((res) => {
res.body.data.length = 1;
res.body.data[0].email = "test@example.com";
})
// Even more logic goes here
});
// More things come here
});
Then, just check if there was an error in the order, otherwise the individual test is finished.
describe("Test example", () => {
test("GET /", (done) => {
request(app)
.get("/")
.expect("Content-Type", /json/)
.expect(200)
.expect((res) => {
res.body.data.length = 1;
res.body.data[0].email = "test@example.com";
})
.end((err, res) => {
if (err) return done(err);
return done();
});
});
// More things come here
});
Basically this is the basis for many others, however we have only tested one of the routes yet, so now we need to test if we can insert data into the database.
So we're going to create a new test called POST /send
, but this time we're going to change the route as well as the method.
describe("Test example", () => {
// Hidden for simplicity
test("POST /send", (done) => {
request(app)
.post("/send")
.expect("Content-Type", /json/)
// More logic goes here
});
// More things come here
});
Now we have to send a JSON body with just one property called email and we know the status code is going to be 201.
describe("Test example", () => {
// Hidden for simplicity
test("POST /send", (done) => {
request(app)
.post("/send")
.expect("Content-Type", /json/)
.send({
email: "francisco@example.com",
})
.expect(201)
// Even more logic goes here
});
// More things come here
});
Now we can check the body of the response, as a new element has been added to the database we know that the length is now two and that the email of the first element must be the initial one and that of the second element must be the same as the one sent .
describe("Test example", () => {
// Hidden for simplicity
test("POST /send", (done) => {
request(app)
.post("/send")
.expect("Content-Type", /json/)
.send({
email: "francisco@example.com",
})
.expect(201)
.expect((res) => {
res.body.data.length = 2;
res.body.data[0].email = "test@example.com";
res.body.data[1].email = "francisco@example.com";
})
// Almost done
});
// More things come here
});
And let's check if an error occurred during the execution of the order, otherwise it's finished. But this time we are going to create a variable to add the id of the second element, so that we can dynamically update and delete it afterwards.
let elementId;
describe("Test example", () => {
// Hidden for simplicity
test("POST /send", (done) => {
request(app)
.post("/send")
.expect("Content-Type", /json/)
.send({
email: "francisco@example.com",
})
.expect(201)
.expect((res) => {
res.body.data.length = 2;
res.body.data[0].email = "test@example.com";
res.body.data[1].email = "francisco@example.com";
})
.end((err, res) => {
if (err) return done(err);
elementId = res.body.data[1].id;
return done();
});
});
// More things come here
});
Now we are going to update an element that was inserted in the database, in this case we are going to use the id that we have stored in the elementId variable. Later we will create a new test, we will define a new route and we will use another http method.
describe("Test example", () => {
// Hidden for simplicity
test("PUT /update/:id", (done) => {
request(app)
.put(`/update/${elementId}`)
.expect("Content-Type", /json/)
// More logic goes here
});
// More things come here
});
In this endpoint we will also send in the JSON body a property called email, however this time we will use another one, as we expect the status code to be 200.
describe("Test example", () => {
// Hidden for simplicity
test("PUT /update/:id", (done) => {
request(app)
.put(`/update/${elementId}`)
.expect("Content-Type", /json/)
.send({
email: "mendes@example.com",
})
.expect(200)
// Even more logic goes here
});
// More things come here
});
In the response code we expect the length of the array to be 2 and that this time the second element must have the value of the new email that was sent.
describe("Test example", () => {
// Hidden for simplicity
test("PUT /update/:id", (done) => {
request(app)
.put(`/update/${elementId}`)
.expect("Content-Type", /json/)
.send({
email: "mendes@example.com",
})
.expect(200)
.expect((res) => {
res.body.data.length = 2;
res.body.data[0].email = "test@example.com";
res.body.data[1].id = elementId;
res.body.data[1].email = "mendes@example.com";
})
.end((err, res) => {
if (err) return done(err);
return done();
});
});
// More things come here
});
Last but not least, let's try to eliminate the element from our database that has the id with the same value as the elementId variable.
The process is similar to what was done in the previous test. But of course, let's define a new route and let's use the appropriate http method.
describe("Test example", () => {
// Hidden for simplicity
test("DELETE /destroy/:id", (done) => {
request(app)
.delete(`/destroy/${elementId}`)
.expect("Content-Type", /json/)
.expect(200)
// More logic goes here
});
});
Now when looking at the response body, this time the array length value should be 1 and the first and only element should be the initial email.
describe("Test example", () => {
// Hidden for simplicity
test("DELETE /destroy/:id", (done) => {
request(app)
.delete(`/destroy/${elementId}`)
.expect("Content-Type", /json/)
.expect(200)
.expect((res) => {
res.body.data.length = 1;
res.body.data[0].email = "test@example.com";
})
.end((err, res) => {
if (err) return done(err);
return done();
});
});
});
The test file (app.test.js
) should look like this:
const request = require("supertest");
const app = require("./app");
let elementId;
describe("Test example", () => {
test("GET /", (done) => {
request(app)
.get("/")
.expect("Content-Type", /json/)
.expect(200)
.expect((res) => {
res.body.data.length = 1;
res.body.data[0].email = "test@example.com";
})
.end((err, res) => {
if (err) return done(err);
return done();
});
});
test("POST /send", (done) => {
request(app)
.post("/send")
.expect("Content-Type", /json/)
.send({
email: "francisco@example.com",
})
.expect(201)
.expect((res) => {
res.body.data.length = 2;
res.body.data[0].email = "test@example.com";
res.body.data[1].email = "francisco@example.com";
})
.end((err, res) => {
if (err) return done(err);
elementId = res.body.data[1].id;
return done();
});
});
test("PUT /update/:id", (done) => {
request(app)
.put(`/update/${elementId}`)
.expect("Content-Type", /json/)
.send({
email: "mendes@example.com",
})
.expect(200)
.expect((res) => {
res.body.data.length = 2;
res.body.data[0].email = "test@example.com";
res.body.data[1].id = elementId;
res.body.data[1].email = "mendes@example.com";
})
.end((err, res) => {
if (err) return done(err);
return done();
});
});
test("DELETE /destroy/:id", (done) => {
request(app)
.delete(`/destroy/${elementId}`)
.expect("Content-Type", /json/)
.expect(200)
.expect((res) => {
res.body.data.length = 1;
res.body.data[0].email = "test@example.com";
})
.end((err, res) => {
if (err) return done(err);
return done();
});
});
});
Now when you run the npm test
command in the terminal, you should get a result similar to this:
Conclusion
I hope it was brief and that you understood things clearly. In the beginning I wasn't a big fan of automated tests but now I practically can't live without them. 🤣
Have a nice day! 👏 ☺️
Top comments (9)
In the put test, you used 2 requests
Thanks for taking time to write this tutorial! It was quite easy to follow.
By the way, doesn't the code below assign values to
res.body.data
rather than test values of its properties? Does supertest (or jest) convert=
to an assertion? Sorry if I'm wrong.Thanks for this article. But does this mean you have to be connected to the internet to be able to run these tests? Or are you hitting a mock local database?
You should create a seperate env and database for testing, with setup and teardown condigured in jest
This was so amazing and helpful. THANK YOU SO MUCH!
You've definitely also got me looking at automated tests with love in my eyes.
Thank you so much for the feedback, I'm glad you liked this article 👊
Just one thing though. When you do this:
.expect((res) => {
res.body.data.length = 1;
res.body.data[0].email = "test@example.com";
})
This isn't confirming the response object properties. I was reading an article today on the same topic (rahmanfadhil.com/test-express-with...) and I think that suggests a more effective way to do it.
Thank you. Please never stop writing.
So you are creating dependent tests? Intentionally? Meaning one test will depend on another test, not just running 1st, but also running successfully, 1st. That's not a good practice.
Very informative read
I was finding it difficult to write tests for a blog-API I was creating, this was really helpful ❤️