Only by writing tests can you understand the significance of testing. No coding Bootcamp or course, as far as I know, teaches you how to write tests. Writing tests may not be necessary for demo projects or coursework projects, but it is critical for real-world applications. So, in this Part 3 of the "Let's build and deploy a full stack MERN web application" series, I'll demonstrate how to write tests for ReactJs components and ExpressJs RESTAPI.
During the testing stage of software development, the application's actual outcomes are compared to those that were expected. Before deploying your application, this phase is used to ensure that everything is functioning as it should. Beginner developers frequently believe that writing tests are a time-consuming and unimportant process. But they are ignorant of the fact that testing actually buys you a lot of time in the long run. You can think of testing as a one-time investment.
Software development uses a variety of testing methods, including Unit Testing, Integration Testing, Beta Testing, Regression Testing, and others. We will learn about Unit Testing in this lesson and how to create tests for applications built with ReactJs and NodeJs.
As the name implies, unit testing involves testing individual pieces of code or an application. For instance, in REST API, we are able to create distinct tests for each endpoint, and in a ReactJs application, we can test each component separately.
Let's begin creating tests for our "Productivity Tracker" application. (If you don't know what this is, read the previous article - Adding Authentication to full stack MERN web application)
Note: I recommend setting up a separate database for testing purposes. But for this tutorial, it's not necessary.
First, we have to separate the server from the application because we are using SuperTest to test the application, not the server.
So create an app.js
file in the root and paste this code.
const express = require("express");
const cors = require("cors");
const ActivityRouter = require("./routes/activity.route");
const AuthRouter = require("./routes/auth.route");
const app = express();
/* Telling the application to use the express.json() middleware. This middleware will parse the body of
any request that has a Content-Type of application/json. */
app.use(express.json());
/* Allowing the frontend to access the backend. */
app.use(cors());
/* This is a route handler. It is listening for a GET request to the root route of the application.
When it receives a request, it will send back a response with the string "Hello World!". */
app.get("/", (req, res) => {
res.send("Hello World!");
});
/* Telling the application to use the ActivityRouter for any requests that start with "/api". */
app.use("/api", ActivityRouter);
/* Telling the application to use the AuthRouter for any requests that start with "/api/auth". */
app.use("/api/auth", AuthRouter);
module.exports = app;
... and in server.js
.
const express = require("express");
const mongoose = require("mongoose");
const app = express();
/* Loading the environment variables from the .env file. */
require("dotenv").config();
const PORT = process.env.PORT || 5000;
const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost/todoapiDB";
/* Connecting to the database and then starting the server. */
mongoose
.connect(MONGODB_URI, { useNewUrlParser: true })
.then(() => {
app.listen(PORT, console.log("Server stated on port 5000"));
})
.catch((err) => {
console.log(err);
});
To start writing tests, you need three npm packages: jest
, supertest
, and cross-env
.
npm i --save-dev jest supertest cross-env
jest: Jest is a framework for testing JavaScript code. Unit testing is its main usage of it. supertest: Using Supertest, we can test endpoints and routes on HTTP servers. cross-env: You can set environmental variables inline within a command using cross-env.
Open your package.json
file and add the test script to the scripts.
"scripts": {
"test": "cross-env NODE_ENV=test jest --testTimeout=5000",
"start": "node server.js",
"dev": "nodemon server.js"
},
In this case, testTimeout
is set to 5000
because it's possible for certain requests to take a while to complete, and cross-env
is being used to set environment variables and jest
to run test suites.
First, create a folder called tests/
at the application's root, and then create a file there called activity.test.js
. Jest searches for the folder tests/
at the project's root when you do npm run test
. As a result, you must place your test files in the tests/
folder.
Next, import the supertest
and mongoose
packages into the test file.
const mongoose = require("mongoose");
const request = require("supertest");
Import dotenv
to load environment variables, and import app.js
as that is where our application starts.
const mongoose = require("mongoose");
const request = require("supertest");
const app = require("../app");
require("dotenv").config();
You'll need to connect and disconnect the database before and after each test (because we don't require the database once testing is complete).
/* Connecting to the database before each test. */
beforeEach(async () => {
await mongoose.connect(process.env.MONGODB_URI);
});
/* Closing database connection after each test. */
afterEach(async () => {
await mongoose.connection.close();
});
Now write your first unit test.
describe("GET /api/activities", () => {
it("should get all the activities", async () => {
const token = await request(app).post("/api/auth/login").send({
email: process.env.EMAIL,
password: process.env.PASSWORD,
});
const response = await request(app)
.get("/api/activities")
.set({
Authorization: "bearer " + token.body.token,
"Content-Type": "application/json",
});
expect(response.statusCode).toBe(200);
expect(response.body.length).toBeGreaterThan(0);
});
});
In the above code,
describe
to describe the unit test. Even though it is not required, it will be useful to identify tests in test results.it
, we write the actual test code. Write the expected result in the first argument, and then in the second argument, write a callback function that contains the test code.Note: Since we have authenticated routes in our application, we need to get the token by logging in to the application and using that token to access the route.
You can write tests for all the endpoints in the same manner.
describe("POST /api/activity", () => {
it("should add an activity to the database", async () => {
const token = await request(app).post("/api/auth/login").send({
email: process.env.EMAIL,
password: process.env.PASSWORD,
});
const response = await request(app)
.post("/api/activity")
.send({
name: "Jogging",
time: "3:00 PM",
})
.set({
Authorization: "bearer " + token.body.token,
"Content-Type": "application/json",
});
expect(response.statusCode).toBe(201);
});
});
Then run npm run test
to run the test suites (suite - test file).
In the same way, let's write tests for React applications. For this, we will be using React Testing Library.
The
@testing-library
family of packages helps you test UI components in a user-centric way.
To start writing tests in React, you need React Testing Library. But these packages come pre-installed with create-react-app (CRA). If you haven't used CRA you can install them like this.
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
It's a good practice to create a test file for every component.
We have 2 components that need to be tested - <App />
and <Login />
. So create App.test.js
and Login.test.js
in the src/
folder. But for now create only the App.test.js
file.
Write your first test.
In the App.jsx
file add the data-testid={"app-header-heading"}
attribute to the main heading element. Later, while writing tests we can refer to this element with this id.
<h1 data-testid="app-header-heading">Productivity Tracker</h1>
Now, open the App.test.js
file and paste the below code.
import { render, screen } from "@testing-library/react";
import App from "./App";
/* This is a test that is testing the App component.
* It is testing that the heading is correct. */
describe("App", () => {
it("should have exact heading", () => {
/* Rendering the App component. */
render(<App />);
/* Getting the element with the test id of "app-header-heading". */
const mainHeading = screen.getByTestId("app-header-heading");
/* Checking that the innerHTML of the element with the test id of "app-header-heading" is equal to
"Productivity Tracker". */
expect(mainHeading.innerHTML).toBe("Productivity Tracker");
});
});
In the above code,
render
and screen
from @testing-library/react
and then imported the component that needs to be tested.it
, we write the actual test code. Write the expected result in the first argument, and then in the second argument, write a callback function that contains the test code.This is the general flow of writing a test for a React component:
Now run the npm test
command. You will see the results.
Please refer to official docs to learn more about React Testing Library and write your own tests.
As a challenge, try to implement the "Add activity" flow. In the next article, we will learn some DevOps stuff.
Also read,
Feel free to ask your doubts in the comments.
Also published here.