Adding Tests to Express Apps with Jest and Supertest
Tags: webdev, programming, javascript, node
Published: 2021-01-24 22:04:58

**Check out my books on Amazon at https://www.amazon.com/John-Au-Yeung/e/B08FT5NT62**

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

Automated tests are essential to the apps we write since modern apps have so many moving parts.

In this piece, we’ll look at how to write apps to test an Express app that interacts with a database with [Jest](https://jestjs.io/) and [SuperTest](https://github.com/visionmedia/supertest).

* * *

Creating the App We’ll Test
===========================

We create a project folder by creating an empty folder and running the following to create a `package.json` file with the default answers:

```
npm init -y
```

Then we run the following to install the packages for our apps:

```
npm i express sqlite3 body-parser
```

Then, we create the `app.js` file for our app and write:

```
const express = require('express');  
const sqlite3 = require('sqlite3').verbose();  
const bodyParser = require('body-parser');  
const app = express();  
const port = process.env.NODE_ENV === 'test' ? 3001 : 3000;  
let db;  
if (process.env.NODE_ENV === 'test') {  
    db = new sqlite3.Database(':memory:');  
}  
else {  
    db = new sqlite3.Database('db.sqlite');  
}

db.serialize(() => {  
    db.run('CREATE TABLE IF NOT EXISTS persons (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');  
});

app.use(bodyParser.json());  
app.get('/', (req, res) => {  
    db.serialize(() => {  
        db.all('SELECT * FROM persons', [], (err, rows) => {  
            res.json(rows);  
        });  
    })  
})

app.post('/', (req, res) => {  
    const { name, age } = req.body;  
    db.serialize(() => {  
        const stmt = db.prepare('INSERT INTO persons (name, age) VALUES (?, ?)');  
        stmt.run(name, age);  
        stmt.finalize();  
        res.json(req.body);  
    })  
})

app.put('/:id', (req, res) => {  
    const { name, age } = req.body;  
    const { id } = req.params;  
    db.serialize(() => {  
        const stmt = db.prepare('UPDATE persons SET name = ?, age = ? WHERE id = ?');  
        stmt.run(name, age, id);  
        stmt.finalize();  
        res.json(req.body);  
    })  
})

app.delete('/:id', (req, res) => {  
    const { id } = req.params;  
    db.serialize(() => {  
        const stmt = db.prepare('DELETE FROM persons WHERE id = ?');  
        stmt.run(id);  
        stmt.finalize();  
        res.json(req.body);  
    })  
})

const server = app.listen(port);  
module.exports = { app, server };
```

The code above has the app we’ll test.

To make our app easier to test, we have:

```
const port = process.env.NODE_ENV === 'test' ? 3001 : 3000;  
let db;  
if (process.env.NODE_ENV === 'test') {  
    db = new sqlite3.Database(':memory:');  
}  
else {  
    db = new sqlite3.Database('db.sqlite');  
}
```

So we can set the `process.env.NODE_ENV` to `'test'` to make our app listen to a different port than it does when the app is running in a nontest environment.

We’ll use the `'test'` environment to run our tests.

Likewise, we want our app to use a different database when running unit tests than when we aren’t running them.

This is why we have:

```
let db;  
if (process.env.NODE_ENV === 'test') {  
    db = new sqlite3.Database(':memory:');  
}  
else {  
    db = new sqlite3.Database('db.sqlite');  
}
```

We specified that when the app is running in a `'test'` environment we want to use SQLite’s in-memory database rather than a database file.

* * *

Writing the Tests
=================

Initialization the code
-----------------------

With the app made to be testable, we can add tests to it.

We’ll use the Jest test runner and SuperTest to make requests to our routes in our tests. To add Jest and SuperTest, we run:

```
npm i jest supertest
```

Then, we add `app.test.js` to the same folder as the `app.js` file we had above.

In `app.test.js`, we start by writing the following:

```
const { app } = require('./app');  
const sqlite3 = require('sqlite3').verbose();  
const request = require('supertest');  
const db = new sqlite3.Database(':memory:');
beforeAll(() => {  
    process.env.NODE_ENV = 'test';  
})
```

In the code above, we included our Express app from our `app.js`. Then we also included the SQLite3 and SuperTest packages.,

Then, we connected to our in-memory database with:

```
const db = new sqlite3.Database(':memory:');
```

Next, we set all the tests to run in the `'test'` environment by running:

```
beforeAll(() => {  
    process.env.NODE_ENV = 'test';  
})
```

This will make sure we use port `3001` and the in-memory database we specified in `app.js` for each test.

To make our tests run independently and with consistent results, we have to clean our database and insert fresh data every time.

To do this, we create a function we call on each test:

```
const seedDb = db => {  
    db.run('CREATE TABLE IF NOT EXISTS persons (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');  
    db.run('DELETE FROM persons');  
    const stmt = db.prepare('INSERT INTO persons (name, age) VALUES (?, ?)');  
    stmt.run('Jane', 1);  
    stmt.finalize();  
}
```

The code above creates the `persons` table if it doesn’t exist and deletes everything from there afterward.

Then we insert a new value in there to have some starting data.

* * *

Adding Tests
============

With the initialization code complete, we can write the tests.

**GET request test**
--------------------

First, we write a test to get the existing seed data from the database with a `GET` request.

We do this by writing:

```
test('get persons', () => {  
    db.serialize(async () => {  
        seedDb(db);  
        const res = await request(app).get('/');  
        const response = [  
            { name: 'Jane', id: 1, age: 1 }  
        ]  
        expect(res.status).toBe(200);  
        expect(res.body).toEqual(response);  
    })  
});
```

We put everything inside the callback of `db.serialize` so the queries will be run sequentially.

First, we call `seedDb`, which we created above to create the table if it doesn’t exist, to clear out the database, and to add new data.

Then, we call the `GET` request by writing:

```
await request(app).get('/');
```

This gets us the `res` object with the response resolved from the promise.

`request(app)` will start the Express app so we can make the request.

Next, we have the `response` for us to check against for correctness:

```
const response = [  
  { name: 'Jane', id: 1, age: 1 }  
]
```

Then, we check the responses to see if we get what we expect:

```
expect(res.status).toBe(200);  
expect(res.body).toEqual(response);
```

The `toBe` method checks for shallow equality, and `toEqual` checks for deep equality. So we use `toEqual` to check if the whole object structure is the same.

`res.status` checks the status code returned from the server, and `res.body` has the response body.

**POST request test**
---------------------

Next, we add a test for the `POST` request. It’s similar to the `GET` request test.

We write the following code:

```
test('add person', () => {  
    db.serialize(async () => {  
        seedDb(db);  
        await request(app)  
            .post('/')  
            .send({ name: 'Joe', age: 2 }); 
        const res = await request(app).get('/');  
        const response = [  
            { name: 'Jane', id: 1, age: 1 },  
            { name: 'Joe', id: 2, age: 2 }  
        ]  
        expect(res.status).toBe(200);  
        expect(res.body).toEqual(response);  
    })  
});
```

First, we reset the database with:

```
seedDb(db);
```

We made our `POST` request with:

```
await request(app)  
  .post('/')  
  .send({ name: 'Joe', age: 2 });
```

This will insert a new entry into the in-memory database.

Finally, to check for correctness, we make the `GET` request — like in our first test — and check if both entries are returned:

```
const res = await request(app).get('/');  
const response = [  
  { name: 'Jane', id: 1, age: 1 },  
  { name: 'Joe', id: 2, age: 2 }  
]  
expect(res.status).toBe(200);  
expect(res.body).toEqual(response);
```

**PUT and DELETE tests**
------------------------

The test for the `PUT` request is similar to the `POST` request. We reset the database, make the `PUT` request with our payload, and then make the `GET` request to get the returned data, as follows:

```
test('update person', () => {  
    db.serialize(async () => {  
        seedDb(db);  
        await request(app)  
            .put('/1')  
            .send({ name: 'Joe', age: 2 }); 
        const res = await request(app).get('/');  
        const response = [  
            { name: 'Jane', id: 1, age: 1 }  
        ]  
        expect(res.status).toBe(200);  
        expect(res.body).toEqual(response);  
    })  
});
```

Then we can replace the `PUT` request with the `DELETE` request and test the `DELETE` request:

```
test('delete person', () => {  
    db.serialize(async () => {  
        seedDb(db);  
        const res = await request(app).delete('/1');  
        const response = [];  
        expect(res.status).toBe(200);  
        expect(res.body).toEqual(response);  
    })  
});
```

* * *

Running the Tests
=================

To run the tests, we add the following to the `scripts` section:

```
"test": "jest --forceExit"
```

We have to add the `--forceExit` option so Jest will exist after the tests are run. There’s no fix for the issue where Jest tests using SuperTest don’t exit properly yet.

Then we run the following to run the tests:

```
npm test
```

And we should get:

```
PASS  ./app.test.js  
  √ get persons (11ms)  
  √ add person (2ms)  
  √ update person (2ms)  
  √ delete person (6ms)Test Suites: 1 passed, 1 total  
Tests:       4 passed, 4 total  
Snapshots:   0 total  
Time:        2.559s  
Ran all test suites.  
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?
```

We should get the same thing no matter how many times we run the tests since we reset the database and made all database queries run sequentially.

Also, we used a different database and port for our tests than other environments, so the data should be clean.

* * *

Conclusion
==========

We can add tests run with the Jest test runner. To do this, we have to have a different port and database for running the tests. Then we create the tables if they don’t already exist, clear all the data, and add seed data so we have the same database structure and content for every test.

With SuperTest, we can run the Express app automatically and make the request we want. Then, we can check the output.