Testing React+Node JS app with Jest and Upload coverage report to CodeClimate with Github Action
Tags: testing, react, node, javascript
Published: 2022-12-01 18:26:26

It is easy to fall into a mindset when we are coding. When writing tests, we often need to consider various situations and make us re-examine our code, which is called Test-Driven-Development.
Writing tests can bring us certain benefits:

- 
To verify the correctness of the code

- 
To avoid errors when modifying code

- 
To avoid errors when other team members modify your code

- 
To facilitate automated testing and deployment

As your app grows larger, it becomes more complex, harder to maintain, and more buggy. Therefore, it's essential to write maintainable and testable code at very early start.
Coverage reports tell you how much code the test cases cover. It provides useful metrics that can help you assess the quality of your test suite. We then can use CodeClimate to analyze the quality of our code from the coverage reports.

## Prerequisites
Suppose you have already created a React+Node JS app with `npm create-react-app` and `npm init`, setup express, and maybe have written some routes.
Likely, you will have a similar folder hierarchy in your monorepo as follows:

```
client
| public
| src
  | App.js
  | index.js
  | app.test.js
  | setupTests.js
  | reportWebVitals.js
| package.json
server
| routes
  | sample.route.js
| package.js
```

## Tesing your react frontend with Jest and React Testing Library

If you are using `create-react-app`, both Jest and React Testing Library are built-in, so we don't need to install them separately.

By default, Jest will look for files with the `.test.js` suffix and files with the `.js` suffix in `__tests__` folders. 

An example `app.test.js` for testing components can be:

```
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

it('renders welcome message', () => {
  render(<App />);
  expect(screen.getByText('Learn React')).toBeInTheDocument();
});
```

## Testing your node backend with Jest and supertest

To start, you will need to first install the following packages by `npm i jest supertest`.

- 
`jest`: Jest is a JavaScript testing framework built on top of Jasmine. Unit testing is the main usage of it.

- 
`supertest`: Supertest is a SuperAgent driven library for testing HTTP servers. With wich, we can test endpoints and routes.

Then in `package.json`, add

```
  "scripts": {
    ...
    "test":  "jest"
  },
```

We will write a sample route first.
In `server/routes/sample.test.js`, suppose we have two functions for getting all users and add user.

```
const express = require("express")
const router = express.Router()
router.get("/getUsers",getUsers)
router.post("/createUser",createUser)

module.exports = router
```

And in `server/tests/sample.test.js`

```
const request = require("supertest");
const express = require("express");
const app = express();
app.use(express.json());
app.use("/", router);
describe("GET /getUsers", () => {
  it("should return all users", async () => {
    const res = await request(app).get("/getUsers");
    expect(res.statusCode).toBe(200);
    expect(res.body.length).toBeGreaterThan(0);
  });
});
describe("POST /createUser", () => {
  it("should not create with empty name", async () => {
    const req = {name: "",password: "123"};
    const res = await request(app).post("/createUser").type('json').send(req);
    expect(res.statusCode).toBe(400);
  });
  it("should create a user ", async () => {
    const req = {name: "user",password: "123"};
    const res = await request(app).post("/createUser").type('json').send(req);
    expect(res.statusCode).toBe(200);
    expect(res.body.name).toBe("user");
  });
});
```

### Testing Express+Mongodb
If you are testing with a mongodb, you should always make sure your database is seperate, that the datasets for each test don't interfere with each other. I would recommand using local mongodb.

You should put your mongodb url in `.env` and import `dotenv` to get the environment variable. Install `dotenv` by `npm i dotenv`.

In `server/.env`

```
MONGODB_URI="mongodb://localhost:27017"
```
You'll need to connect and disconnect the database before and after each test.
In `server/tests/mongo.test.js`
```
const mongoose = require("mongoose");
const request = require("supertest");
const app = require("../app");
require("dotenv").config("../.env");

beforeEach(async () => {
  await mongoose.connect(process.env.MONGODB_URI);
});

afterEach(async () => {
  await mongoose.connection.dropDatabase();
  await mongoose.connection.close();
});
```

## Generate test coverage report 
In the [Code Climate](https://docs.codeclimate.com/docs/configuring-test-coverage) documentation, the supported coverage tool for JavaScript is lcov (generated by Istanbul).
In both `client` and `server` folders:

- 
At root, add the nyc package with `npm install --save-dev nyc`

- 
In `package.json`, add:

```
  "jest": {
    "coverageReporters": [
      [
        "lcov",
        {
          "projectRoot": ".."
        }
      ]
    ]
  },
  "nyc": {
    "reporter": [
      "lcov"
    ]
  }
```
   To make `jest` and `nyc` generate `lcov` reports. You can also add other reporters like `text`, `text-summary`.

- 
For your react frontend, add to the `package.json`:

```
  "scripts": {
    ...
    "coverage":  "react-scripts test --coverage --watchAll=false"
  },
  ...
  "jest": {
    ...
    "transformIgnorePatterns": ["node_modules\/(?!axios)"],
    "collectCoverageFrom": [
      "src/**/*.js",
      "!**/node_modules/**",
    ]
  }
```
At the root of `client` folder, run `npm coverage` to see the coverage report.

- 
Similarly in the node backend, add to the `scripts` in `package.json`:
```
  "scripts": {
    ...
    "coverage": "jest --coverage"
  },
  ...
  "jest": {
    ...
    "transformIgnorePatterns": ["node_modules\/(?!axios)"],
    "testEnvironment": "node",
    "collectCoverageFrom": [
      "**/routes/**/*.js",
      "!**/node_modules/**",
    ]
  }
```

At the root of `server` folder, run `npm coverage` to see the coverage report.

We can use the property `collectCoverageFrom` to specify which files need to be collected, and which files should not be collected (`node_modules`, `config.js`, datasets, and so on).

## Upload coverage report to CodeClimate

Note that you need at least some test coverage to generate a report that is able to be uploaded to CodeClimate, so make sure you followed the previous parts carefully.

To start, make sure you already have a CodeClimate account.

On the dashboard, click on "Add a repository" and then "add repo" to link your repo to CodeClimate.

Go to `repo setting/test coverage` and find the `TEST REPORTER ID`

In your Github repo, click on `setting/secrets/action`, and add the `CC_TEST_REPORTER_ID` as a new secret key.

Below is an example of the `github-action.yml`

```
name: build
on: 
  workflow_dispatch:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
jobs:
  Coverage:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    strategy:
      matrix:
        node-version: [14.x]
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - name: Test Coverage Frontend
      run: |
        cd client
        npm install
        npm coverage

    - name: Test Coverage Backend
      run: |
        cd server
        cp -r .env.example .env
        npm install
        npm coverage
        
    - name: Test & publish code coverage
      uses: paambaati/codeclimate-action@v3.2.0
      env:
        CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}}
      with:
        coverageLocations: |
          ${{github.workspace}}/client/coverage/lcov.info:lcov
          ${{github.workspace}}/server/coverage/lcov.info:lcov
```
In order to upload the two coverage tests for react and node at once, we need to set the `projectRoot` of the `lcov` coverage reporter of jest to the root of our monorepo (using ".." in `package.json`).