Module 4: Backend Testing
Test Web API Endpoints
The tests we write for endpoints are called integration tests because they test how different parts of the system work together. This is different from the unit tests we use to verify the correctness of one part of the system in isolation.
We'll use a npm module called supertest that makes it easier to write tests for Node.js HTTP servers. We can use supertest to load an instance of our server, send requests to the different endpoints, and make assertions about the responses.
We could use supertest to verify that making a POST request to a particular endpoint returns a 201 HTTP status code after successfully creating a resource or, that it returns a 500 code if the server ran into an error while processing the request. Writing such a test may look like this:
- Save a reference to the server.
- Use supertest to make a POST request passing correct data inside the body.
- Check that the server responds with status code 201.
Writing such a test verifies that all middleware, including the route handler is working as intended.
How to Build It
- Create a folder and cd into it:
mkdir api-testing cd api-testing
- Initialize git repository:
git init
- Create package.json:
npm init -y
- Install dependencies:
npm i express npm i -D jest supertest
- Update package.json with test script and jest config:
{ "scripts": { "test": "jest --watch --verbose" }, "jest": { "testEnvironment": "node" } }
- Create server.spec.js:
const request = require('supertest'); const server = require('./server.js'); describe('server.js', () => { describe('index route', () => { it('should return an OK status code from the index route', async () => { const expectedStatusCode = 200; const response = await request(server).get('/'); expect(response.status).toEqual(expectedStatusCode); }); it('should return a JSON object from the index route', async () => { const expectedBody = { api: 'running' }; const response = await request(server).get('/'); expect(response.body).toEqual(expectedBody); }); it('should return a JSON object from the index route', async () => { const response = await request(server).get('/'); expect(response.type).toEqual('application/json'); }); }); });
- Create server.js:
const express = require('express'); const server = express(); server.get('/', (req, res) => { res.status(200).json({ api: 'running' }); }); module.exports = server;
Note: We separate the server implementation from the code that runs the server to avoid port conflicts during testing and to maintain separation of concerns between building and running the server.
Test Data Access Code
To test the data access, we'll write end to end tests. These types of tests run slower because they perform operations and run queries against an actual database that is similar to the one used in production.
To avoid polluting the development database, we'll use a separate database for testing. One advantage of using a dedicated testing database is that we can add and remove records without affecting the data in the development or staging databases.
Next, we'll walk through setting up our API to switch to the testing database based on an environment variable, including setting a different value for that environment value only when running tests.
How to Build It
Using cross-env
Setting and using environment variables is different for Windows and POSIX (Mac, Linux, Unix) Operating Systems. We can use cross-env. This npm module deals with the OS inconsistencies and provides a uniform setting environment variables across all platforms.
Open package.json and look at the test script. It uses cross-env to set an environment variable with the key DB_ENV and the value: testing.
"test": "cross-env DB_ENV=testing jest --watch"
This environment variable is available to the API as process.env.DB_ENV. The sample code provided uses it inside ./data/dbConfig.js to choose which configuration object to use for the knex connection.
// ./data/dbConfig.js
const knex = require('knex');
const config = require('../knexfile.js');
// if the environment variable is not set, default to 'development'
// this variable is only set when running the "test" npm script using npm run test
const dbEnv = process.env.DB_ENV || 'development';
// the value of dbEnv will be either 'development' or 'testing'
// we pass it within brackets to select the corresponding configuration
// from knexfile.js
module.exports = knex(config[dbEnv]);
To make this work, knexfile.js has a dedicated configuration key for testing.
// ./knexfile.js
module.exports = {
development: {
client: 'sqlite3',
connection: {
filename: './data/hobbits.db3',
},
useNullAsDefault: true,
migrations: {
directory: './data/migrations',
},
seeds: {
directory: './data/seeds',
},
},
testing: {
client: 'sqlite3',
connection: {
filename: './data/test.db3',
},
useNullAsDefault: true,
migrations: {
directory: './data/migrations',
},
seeds: {
directory: './data/seeds',
},
},
};
Using Different Environments for Knex Migrations and Seeding
Different databases have different configuration objects defined within knexfile.js. To specify which environment to target during migrations and seeding use the --env command-line argument.
To run migrations against the testing database, use the following command.
npx knex migrate:latest --env=testing
To run seeds against the testing database, use the following command.
npx knex seed:run --env=testing
Write End to End Tests that Involve the Database
To test the data access code, execute the data access and verify that the database was updated correctly.
In this example, the data access code has an .insert() method. Let's see how to test it.
// our connection to the database
const db = require('../data/dbConfig.js');
// the data access file we are testing
const Hobbits = require('./hobbitsModel.js');
describe('hobbits model', () => {
describe('insert()', () => {
// this example uses async/await to make it easier to read and understand
it('should insert the provided hobbits into the db', async () => {
// this code expects that the table is empty, we'll handle that below
// add data to the test database using the data access file
await Hobbits.insert({ name: 'gaffer' });
await Hobbits.insert({ name: 'sam' });
// read data from the table
const hobbits = await db('hobbits');
// verify that there are now two records inserted
expect(hobbits).toHaveLength(2);
});
});
});
To guarantee that the tables are cleared before running each test, add the following code before the test cases.
beforeEach(async () => {
// this function executes and clears out the table before each test
await db('hobbits').truncate();
});
Implement code to make the tests pass.
// ./hobbits/hobbitsModel.js
async function insert(hobbit) {
// the second parameter here is of other databases, SQLite returns the id by default
const [id] = await db('hobbits').insert(hobbit, 'id');
return db('hobbits')
.where({ id })
.first();
}
This test checks that two records are added to the table, even if those records were there at the beginning.
Let's add another test to make sure that the record is making it to the database and that the .insert() method returns the newly inserted hobbit.
// note we're checking one hobbit at a time
it('should insert the provided hobbit into the db', async () => {
let hobbit = await Hobbits.insert({ name: 'gaffer' });
expect(hobbit.name).toBe('gaffer');
// note how we're reusing the hobbit variable
hobbit = await Hobbits.insert({ name: 'sam' });
expect(hobbit.name).toBe('sam');
});
Module 4 Project: Backend Testing
The module project contains advanced problems that will challenge and stretch your understanding of the module's content. The project has built-in tests for you to check your work, and the solution video is available in case you need help or want to see how we solved each challenge, but remember, there is always more than one way to solve a problem. Before reviewing the solution video, be sure to attempt the project and try solving the challenges yourself.
Instructions
The link below takes you to Bloom's code repository of the assignment. You'll need to fork the repo to your own GitHub account, and clone it down to your computer:
Starter Repo: Unit Testing
- Fork the repository,
- clone it to your machine, and
- open the README.md file in VSCode, where you will find instructions on completing this Project.
- submit your completed project to the BloomTech Portal