Test-driven development, or TDD, is a fairly common style of programming in the workplace, and, in many instances, is mandatory for certain projects. Luckily, TDD is pretty straightforward when you understand the fundamentals behind the process.
There are also many libraries and frameworks used for backend testing in JavaScript. Two of the most popular ones are Jest and Mocha, with alternatives of Jasmine, Ava, Tape, and QUnit.
Node.js now has its own inbuilt test runner that’s been stable since v18. However, it is still in experimental mode, so it is likely to change over time.
In this article, we’ll discuss how to use the new Node.js test runner for some basic testing, as well as using Jest for testing different endpoints.
First, let’s jump into testing on the backend using Node.js.
If you want to use the inbuilt Node.js test runner, you need to import the library below and follow the Node.js documentation for running the tests:
// JavaScript ESM Modules syntax import test from 'node:test'; // JavaScript CommonJS syntax const test = require('node:test');
Of course, you also need to make sure that you are using v18 or later. You can check which version you’re currently running in the command line using node -v
.
Firstly, let’s see what it’s like to do testing using the built in Node.js test runner.
Open your BASH application and cd
to the directory of your choice. Run the commands below to scaffold your project:
mkdir node-backend-testing cd node-backend-testing npm init -y touch index.js index.test.js
Next, open the newly created project in your code editor and add the code to the files they relate to.
Below is the code for the file index.js
:
const calcAge = (dob) => { const digits = { year: 'numeric', }; const year = new Date().toLocaleDateString('en-US', digits); console.log(year); return year - dob; }; const createBox = (x, y) => { return x * y; }; const canDrive = () => { const age = 18; if (age >= 18) { return 'Full Driving Licence'; } else { return 'Provisional License'; } }; const powerLevel = () => { const power = 9001; if (power > 9000) { return true; } else { return false; } }; const workSchedule = (employeeOne, employeeTwo) => { return employeeOne + employeeTwo; }; module.exports = { calcAge, createBox, canDrive, powerLevel, workSchedule, };
Next, we have the code for index.test.js
:
const test = require('node:test'); const assert = require('assert/strict'); const { calcAge, createBox, canDrive, powerLevel, workSchedule, } = require('./index'); // Calculates how old someone is and depending on the year this test could pass or fail test('calculates age', () => { return assert.equal(calcAge(2000), 22); }); // Creates a box with an equal height and width test('creates a box', async (t) => { await t.test('creates a small box', () => { assert.equal(createBox(10, 10), 100); }); await t.test('creates a large box', () => { assert.equal(createBox(50, 50), 2500); }); }); // Checks to see whether or not the person has a full driving licence test('checks license', () => { return assert.match(`${canDrive()}`, /Full Driving Licence/); }); // Confirms that the person has a power level that is over 9000! test('confirms power level', () => { return assert.ok(powerLevel()); }); // Checks to see if the employees have the same amount of shift work days in a week test('employees have an equal number of work days', () => { const employeeOne = ['Monday', 'Tuesday', 'Wednesday,', 'Thursday']; const employeeTwo = ['Friday', 'Saturday', 'Sunday,', 'Monday']; return assert.equal(workSchedule(employeeOne.length, employeeTwo.length), 8); });
Finally, add this run script for the file package.json
:
"scripts": { "test": "node index.test.js" },
The test script runs the Node test runner, which will then run the tests in index.test.js
.
You’ll only need to run one command from inside the root folder. Once again, you need to be using Node v18 or later for this to work.
Run the command below and you should see five tests passing in the console:
npm run test
You can play around with the code inside of the index.js
and index.test.js
files to see the tests pass and fail. If you look at the test errors in the console you will know what’s failing why.
Let me give you some examples below.
To calculate a user’s age, use the current year minus their year of birth. See the example below in the index.test.js
file:
test('calculates age', () => { return assert.equal(calcAge(2000), 21); });
To see the test fail, enter an incorrect age like 21
. The function is expecting a return value of 22
in this example, so the number 21
makes the test fail.
This test is expecting answers of 100
and 2500
for the equations 10 x 10
and 50 x 50
, respectively. Enter in values that don’t add up to the correct output to see the test fail.
test('creates a box', async (t) => { await t.test('creates a small box', () => { assert.equal(createBox(10, 30), 100); }); await t.test('creates a large box', () => { assert.equal(createBox(50, 20), 2500); }); });
This test checks to see if a person has a valid driver’s licence. Change the age in the index.js
file’s canDrive
function to a value less than 18
. The test will fail.
const canDrive = () => { const age = 17; if (age >= 18) { return 'Full Driving Licence'; } else { return 'Provisional License'; } };
This test checks to see if a person has a power level that over 9000 (did you catch the Dragon Ball Z reference? 😁)
Change the power level inside of the index.js
file’s powerLevel
function to less than 9000
. The test will fail.
const powerLevel = () => { const power = 5000; if (power > 9000) { return true; } else { return false; } };
This tests checks that employees are working an equal amount of days.
Two employees are currently working four days each, giving us a total of eight days combined. To see the test fail, just enter or remove some days from the arrays so they are no longer the same length.
test('employees have an equal number of work days', () => { const employeeOne = ['Monday', 'Tuesday', 'Wednesday,', 'Thursday']; const employeeTwo = ['Friday', 'Saturday']; return assert.equal(workSchedule(employeeOne.length, employeeTwo.length), 8); });
Now, let’s do some backend testing using the Jest testing library.
Open your BASH application and cd
to your preferred directory. Run the commands below to scaffold your project:
mkdir jest-backend-testing cd jest-backend-testing npm init -y npm i express http-errors jest nodemon supertest mkdir routes touch routes/products.js touch app.js app.test.js server.js
Now, open the project in your code editor and add the code below into their corresponding files.
Below is code for the file routes/product.js
:
const express = require('express'); const router = express.Router(); const createError = require('http-errors'); // Products Array const products = [{ id: '1', name: 'Playstation 5', inStock: false }]; // GET / => array of items router.get('/', (req, res) => { res.json(products); }); // GET / => items by ID router.get('/:id', (req, res, next) => { const product = products.find( (product) => product.id === String(req.params.id) ); // GET /id => 404 if item not found if (!product) { return next(createError(404, 'Not Found')); } res.json(product); }); router.post('/', (req, res, next) => { const { body } = req; if (typeof body.name !== 'string') { return next(createError(400, 'Validation Error')); } const newProduct = { id: '1', name: body.name, inStock: false, }; products.push(newProduct); res.status(201).json(newProduct); }); module.exports = router;
Next, the code for the file app.js
:
const express = require('express'); const productsRoute = require('./routes/products'); const app = express(); app.use(express.urlencoded({ extended: false })); app.use(express.json()); app.use('/', productsRoute); module.exports = app;
Below is the code for the file app.test.js
:
const request = require('supertest'); const app = require('./app'); describe('GET /', () => { it('GET / => array of items', () => { return request(app) .get('/') .expect('Content-Type', /json/) .expect(200) .then((response) => { expect(response.body).toEqual( expect.arrayContaining([ expect.objectContaining({ id: expect.any(String), name: expect.any(String), inStock: expect.any(Boolean), }), ]) ); }); }); it('GET / => items by ID', () => { return request(app) .get('/1') .expect('Content-Type', /json/) .expect(200) .then((response) => { expect(response.body).toEqual( expect.objectContaining({ id: expect.any(String), name: expect.any(String), inStock: expect.any(Boolean), }) ); }); }); it('GET /id => 404 if item not found', () => { return request(app).get('/10000000000').expect(404); }); it('POST / => create NEW item', () => { return ( request(app) .post('/') // Item send code .send({ name: 'Xbox Series X', }) .expect('Content-Type', /json/) .expect(201) .then((response) => { expect(response.body).toEqual( expect.objectContaining({ name: 'Xbox Series X', inStock: false, }) ); }) ); }); it('POST / => item name correct data type check', () => { return request(app).post('/').send({ name: 123456789 }).expect(400); }); });
Almost there! Here’s the code for the server.js
file:
const app = require('./app'); const port = process.env.PORT || 3000; app.listen(port, () => console.log(`Server running on port ${port}, http://localhost:${port}`) );
Finally, add these run scripts to your package.json
file.
"scripts": { "start": "node server.js", "dev": "nodemon server.js", "test": "jest --watchAll" },
The start
script runs the server file using node.
The dev
script runs the server file using nodemon
, enabling automatic reloading.
The test
script runs the test runner Jest that automatically watches for file changes.
It’s time to start the application and test runner. Run the commands below in different tabs or windows so you have one script running the development server and the other running the Jest test runner.
The server is running on http://localhost:3000/:
npm run dev npm run test
You should now have the dev
server running. The Jest test runner should also be running with five passing tests. Let’s go through each of the tests you can see them pass or fail.
The tests have the same name as the headings, so they should be fairly easy to find in the files.
This test checks to see if an array of objects is returned. To see it fail, open the products.js
file and replace the route at the top with the code below:
router.get('/', (req, res) => { res.send('products'); });
You should get the error expected Content-Type" matching /json/, got "text/html; charset=utf-8
and the test should fail.
Now try changing it to the code below:
router.get('/', (req, res) => { res.json('products'); });
You should get this error Expected: ArrayContaining [ObjectContaining {"id": Any<String>, "inStock": Any<Boolean>, "name": Any<String>}]
.
id
testThis test checks to see if an item is returned with the correct id
. Change the products
array at the top of the file in products.js
to an id
that is a number (instead of a string) to see it fail:
const products = [{ id: 1, name: 'Playstation 5', inStock: false }];
You should get this error: expected "Content-Type" matching /json/, got "text/html; charset=utf-8"
.
This test checks to see if an item can’t be found. To see it fail, change the 404 error
code to something else in the products.js
file, like in the example below:
if (!product) { return next(createError(400, 'Not Found')); }
You should get the error: expected 404 "Not Found", got 400 "Bad Request"
.
This test checks if the object has correct data and data types. To see it fail, open the app.test.js
file and replace the send code with the code below:
.send({ name: 'Nintendo Switch', })
You should see this error:
- Expected - 2 + Received + 3 - ObjectContaining { + Object { + "id": "1", "inStock": false, - "name": "Xbox Series X", + "name": "Nintendo Switch",
This test checks to see if the name variable is the correct data type. It should only fail if a string is detected. To see the test fail, open the app.test.js
file, scroll to the bottom, and change the number to a string like this:
return request(app).post('/').send({ name: '123456789' }).expect(400);
You should get this error: expected 400 "Bad Request", got 201 "Created"
.
Jest 28 was released in April 2022 and brought along a lot of new features. One of the most requested features is called sharding. Essentially, sharding lets you split your test suite into different shards to run a fraction of your suite tests.
For example, to run a third of your tests, you could use the commands below:
jest --shard=1/3 jest --shard=2/3 jest --shard=3/3
That’s all! We just went over an introduction to testing on the backend using Node and Jest. What we covered are just the basics of testing – there is so much more to learn! You can check out the official documentation for Jest here.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowFix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.
From basic syntax and advanced techniques to practical applications and error handling, here’s how to use node-cron.
The Angular tree view can be hard to get right, but once you understand it, it can be quite a powerful visual representation.
In this post, we’ll compare Babel and SWC based on setup, execution, and speed.
One Reply to "Node.js Express test-driven development with Jest"
Why is it called a test driven development, it appears to me as development driven testing, since your first wrote the logic and then tests to test that logic. I heard that test driven development necessarily create test suits before actual development and then logic is written after wards in accordance to the testing criteria, is that true?