Andrew Baisden I’m a full-stack developer from London. Programming is one of my passions in life. A majority of my most recent skills have been self-taught. I find this is one of the best ways to learn because you can go at your own pace.

Node.js Express test-driven development with Jest

7 min read 2149

Node.js Express Test-Driven Development With Jest

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.

Testing on the backend

First, let’s jump into testing on the backend using Node.js.

Using the Node.js test runner to test backend code

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.

Project setup

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.

We made a custom demo for .
No really. Click here to check it out.

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.

Running Node tests

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.

Calculate age test

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.

Create a box test

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

Check license test

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

Confirm power level test

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

Same number of work days test

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

Using Jest to test backend code

Now, let’s do some backend testing using the Jest testing library.

Project setup

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.

Running Jest tests

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.

Array of items test

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>}].

Items by id test

This 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".

Item not found test

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

Correct data test

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",

Correct data type test

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: New features

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

Conclusion

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.

200’s only Monitor failed and slow network requests in production

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. https://logrocket.com/signup/

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. .
Andrew Baisden I’m a full-stack developer from London. Programming is one of my passions in life. A majority of my most recent skills have been self-taught. I find this is one of the best ways to learn because you can go at your own pace.

Leave a Reply