How to write tests in full-stack MERN web application

How to write tests in full-stack MERN web application

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.

Let's get started

What is testing?

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.

What is Unit Testing?

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)

Writing tests in NodeJs application with Jest and SuperTest

Note: I recommend setting up a separate database for testing purposes. But for this tutorial, it's not necessary.

Step 1: Separate application and server

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);
  });

Step 2: Install packages

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.

Step 3: Add test script

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.

Step 4: Start writing tests

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,

  • We use describe to describe the unit test. Even though it is not required, it will be useful to identify tests in test results.
  • In 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.
  • In the callback function, the request is sent to the endpoint first, and the expected and actual responses are then compared. The test passes if both answers match, else, it fails.

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).

test results

Writing tests in ReactJs application with React Testing Library

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.

Step 1: Install packages

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

Step 2: Create test files

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.

Step 3: Start writing tests

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,

  • We first imported the required packages - render and screen from @testing-library/react and then imported the component that needs to be tested.
  • To describe the unit test, we use the same syntax as in express. Most React developers, by convention, put the component name in the first argument. The actual test is written as a callback function in the second argument.
  • In 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.
  • Rest of the code is self-explanatory and you can also understand from the comments. We are expecting the App component should contain a heading called "Productivity Tracker".

This is the general flow of writing a test for a React component:

  • Render the component -> Write elements you want to interact with -> Interact with those elements -> Assert that the outcomes are as expected.

Now run the npm test command. You will see the results.

App component test 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.


Enjoyed reading this? Follow for more.

Did you find this article valuable?

Support itsrakesh by becoming a sponsor. Any amount is appreciated!