Categories
JavaScript Testing

Running Repetitive Test Code with Jest

In our unit tests, we often have to write test code that runs before and after each test. We’ve to write them often to run code to set up fixtures before tests and run code to clean up everything after tests.

We can easily do this with Jest since it comes with a few hooks to do this.

In this article, we’ll look at how to write repetitive setup and teardown code in a way that isn’t repetitive.

How to Write Setup and Teardown Code

With Jest, we can write setup and teardown code by using the beforeEach and afterEach hooks.

beforeEach runs code before each test, and afterEach runs code after each test. The order applies inside a describe block and if there’s no describe block, for the whole file.

For example, given that we have the following code in example.js :

const getStorage = (key) => localStorage.getItem(key);  
const setStorage = (key, value) => localStorage.setItem(key, value);  
module.exports = {  
    getStorage,  
    setStorage  
}

We write the test for them as follows:

const { getStorage, setStorage } = require('./example');

beforeEach(() => {  
    localStorage.setItem('foo', 1);  
    expect(localStorage.getItem('bar')).toBeNull();  
});

afterEach(() => {  
    localStorage.clear();  
});

test('getStorage("foo") is 1', () => {  
    expect(getStorage('foo')).toBe('1');  
});

test('setStorage saves data to local storage', () => {  
    setStorage('bar', 2);  
    const bar = +localStorage.getItem('bar');  
    expect(getStorage('foo')).toBe('1');  
    expect(bar).toBe(2);  
});

We pass a callback function into the beforeEach hook to run code before each test that we have below.

In this example, we prepopulate local storage with an item with key 'foo' and value '1' .

We also check that local storage doesn’t have the item with key 'bar’ with:

expect(localStorage.getItem('bar')).toBeNull();

All tests will pass with the expect we have above since we’ll run localStorage.clear() in the afterEach hook.

Likewise, we pass in a callback to afterEach to run code after each test is run.

We clear local storage with:

localStorage.clear();

Then when running the tests, we get that item with key'bar' is null since we didn’t populate it in the first test.

In the second test, we’ll get that expect(getStorage(‘foo’)).toBe(‘1’); passing since we populated the local storage with it in our beforeEach hook.

Then since we ran:

setStorage('bar', 2);

to to save an item with key 'bar' and value '2' , we’ll get that:

expect(bar).toBe(2);

passing since we saved the item in the test.

Asynchronous Code

In the example above, we ran synchronous code in our hooks. We can also run asynchronous code in our hooks if they either take a done parameter or return a promise.

If we want to run a function that takes a callback and runs it asynchronously as follows:

const asyncFn = (callback) => {  
    setTimeout(callback, 500);  
}

Then in the beforeEach hook we can run asyncFn as follows:

const { asyncFn } = require('./example');

beforeEach((done) => {  
    const callback = () => {  
        localStorage.setItem('foo', 1);  
        done();  
    }  
    asyncFn(callback);  
    expect(localStorage.getItem('bar')).toBeNull();  
});

It does the same thing as the previous beforeEach callback except it’s done asynchronously. Note that we call done passed in from the parameter in our callback.

If we omit, it, the tests will fail as they time out. The code in the tests is the same as before.

We can wait for promises to resolve by returning it in the callback we pass into the hooks.

For example, in example.js , we can write the following function to run a promise:

const promiseFn = () => {  
    return new Promise((resolve) => {  
        localStorage.setItem('foo', 1);  
        resolve();  
    });  
}

Then we can put promiseFn in module.exports and then run it in our beforeEach by running:

beforeEach(() => {  
    expect(localStorage.getItem('bar')).toBeNull();  
    return promiseFn();  
});

We ran promiseFn which we imported before this hook and we return the promise returned by that function, which sets the local storage like the first example except it’s done asynchronously.

Then after we put everything together, we have the following code in example.js , which we run in our test code in the hooks and for testing:

const getStorage = (key) => localStorage.getItem(key);  
const setStorage = (key, value) => localStorage.setItem(key, value);  
const asyncFn = (callback) => {  
    setTimeout(callback, 500);  
}  
const promiseFn = () => {  
    return new Promise((resolve) => {  
        localStorage.setItem('foo', 1);  
        resolve();  
    });  
}  
module.exports = {  
    getStorage,  
    setStorage,  
    asyncFn,  
    promiseFn  
}

Then we have asyncExample.test.js to change the hook to be asynchronous:

const { getStorage, setStorage, asyncFn } = require('./example');

beforeEach((done) => {  
    const callback = () => {  
        localStorage.setItem('foo', 1);  
        done();  
    }  
    asyncFn(callback);  
    expect(localStorage.getItem('bar')).toBeNull();  
});

afterEach(() => {  
    localStorage.clear();  
});

test('getStorage("foo") is 1', () => {  
    expect(getStorage('foo')).toBe('1');  
});

test('setStorage saves data to local storage', () => {  
    setStorage('bar', 2);  
    const bar = +localStorage.getItem('bar');  
    expect(getStorage('foo')).toBe('1');  
    expect(bar).toBe(2);  
});

Then in example.test.js we have:

const { getStorage, setStorage } = require('./example');

beforeEach(() => {  
    localStorage.setItem('foo', 1);  
    expect(localStorage.getItem('bar')).toBeNull();  
});

afterEach(() => {  
    localStorage.clear();  
});

test('getStorage("foo") is 1', () => {  
    expect(getStorage('foo')).toBe('1');  
});

test('setStorage saves data to local storage', () => {  
    setStorage('bar', 2);  
    const bar = +localStorage.getItem('bar');  
    expect(getStorage('foo')).toBe('1');  
    expect(bar).toBe(2);  
});

and finally in promiseExample.test.js we have:

const { getStorage, setStorage, promiseFn } = require('./example');

beforeEach(() => {  
    expect(localStorage.getItem('bar')).toBeNull();  
    return promiseFn();  
});

afterEach(() => {  
    localStorage.clear();  
});

test('getStorage("foo") is 1', () => {  
    expect(getStorage('foo')).toBe('1');  
});

test('setStorage saves data to local storage', () => {  
    setStorage('bar', 2);  
    const bar = +localStorage.getItem('bar');  
    expect(getStorage('foo')).toBe('1');  
    expect(bar).toBe(2);  
});

BeforeAll and AfterAll

To only run the setup and teardown code once in each file, we can use the beforeAll and afterAll hooks. We can pass in a callback with the code that we want to run like we did with the beforeEach and afterEach hooks.

The only difference is that the callbacks are run once before the test in a file is run and after the test in a file is run instead of running them before and after each test.

Callback of beforeAll runs after the callback for beforeEach and callback for afterAll runs after the callback forafterEach .

The order applies inside a describe block and if there’s no describe block, for the whole file.

Running Only One Test

We can write test.only instead of test to run only one test. This is handy for troubleshooting since we don’t have to run all the tests and it helps us pin issues with the test that’s failing.

To write test code that needs to be run for all tests, we use the beforeEach and afterEach hooks in Jest.

To write test code that’s only run per describe block or file, we can use the beforeAll and afterAll hooks.

Callback of beforeAll runs after the callback for beforeEach and callback for afterAll runs after the callback forafterEach .

Categories
Express JavaScript Testing

Adding Tests to Express Apps with Jest and Supertest

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

Categories
Express JavaScript Testing

Adding Database Interactions to Express Apps

Most back end apps need to interact with a database to do something useful. With Express apps, we can add database interactivity easily.

In this article, we’ll look at how to get and save data into an SQLite database with an Express app.

Getting Started

We get started by creating a project folder, then go in it and run:

npm init -y

to create a package.json file.

The -y flag just answers all the question from npm init with the default options.

Next, we install Express, body-parser to parse request bodies, and sqlite3 for interacting with our SQLite database by running:

npm i express sqlite3 body-parser

Building Our App

Initialization Code

The app we build will get and save data to the persons database.

To start, we create an app.js for the app. Then we create the app by first importing the packages we need to use and setting the port that our app will listen to connections to.

To do this, we add:

const express = require('express');  
const sqlite3 = require('sqlite3').verbose();  
const bodyParser = require('body-parser');  
const app = express();  
const port = 3000;

We include the packages we installed earlier, including Express, body-parser , and sqlite3 .

Then we create an instance of our Express app and set the port to 3000 to listen to requests on port 3000.

Next, we create our database initialization code. We do this by writing:

const db = new sqlite3.Database('db.sqlite');

The code above tells us that our app will use db.sqlite to get and save data. If it doesn’t exist, it’ll be created on the fly so we don’t have to create it beforehand. Also, we don’t need any credentials to use the database file.

Then we create the person table if it doesn’t already exist by writing:

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

We have an id auto-incrementing integer column, and name and age columns for saving some personal data.

We include IF NOT EXISTS in our CREATE TABLE query so that the database won’t be attempted to be created every time the app restarts. Since if it already exists, the query will fail if the table already exists.

Then we include our body-parser middleware so that JSON request bodies will be parsed into the req.body object as follows:

app.use(bodyParser.json());

Adding the Routes

Now we add the routes for getting and saving data.

First, we add a route to handle GET requests for getting data from the persons table, we add the following:

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

We use app.get to handle the GET request to the / route.

db.serialize makes sure that each query inside the callback runs in sequence.

Then we select all the rows from the persons table and send the array returned as the response.

Next, we add our POST route to handle the POST request for saving a new entry to the database:

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

We start by using the app.post to indicate that we’re handling a POST request. The / means that we’ll handle requests to the / path.

Next, we get the parsed JSON request body returned by body-parser by writing:

const { name, age } = req.body;

We decomposed the fields into variables with the destructing assignment operator.

Next, we create a prepared statement to insert data to the persons table with db.prepare. This lets us set data for the placeholders marked by the ? and also sanitizes the data to avoid SQL injection attacks.

Then we run:

stmt.run(name, age);  
stmt.finalize();

to run the statement and return the response with res.json(req.body); .

Next, we add the route to let us update an entry:

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

It’s similar to the POST request route above, except that we’re handling PUT requests.

The difference is that we have an :id in the URL parameter, which we get by writing:

const { id } = req.params;

So when we make a request to /1 , id will be set to 1.

Then we ran our update statement with name , age , and id and return the request body as the response.

Finally, we have our DELETE route to let us delete items from the persons table as follows:

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

It’s similar to the other routes except that we have a delete request and we run a delete statement with the ID.

Then we return the request body as the response.

Finally, we add the following line:

const server = app.listen(port);

So that when we run node app.js , our app will run.

Conclusion

We run our app by running node app.js .

In the end, we have the following code if we put everything together:

const express = require('express');  
const sqlite3 = require('sqlite3').verbose();  
const bodyParser = require('body-parser');  
const app = express();  
const port = 3000;  
const 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);

Once we include a database library, we can create an Express app that interacts with a database to make our apps more useful.