Javascript integration and unit tests

Javascript integration (left) and unit tests (right).

Introduction

This blog post is a continuation for my two earlier blog posts:

In this new blog post I continue my Javascript / Typescript story implementing some unit and integration tests for my Javascript backend.

The demo app is in my Github repo: js-node-ts-react.

For experienced Javascript programmers, there is nothing new in this blog post. I created this exercise and this blog post just for my personal learning purposes to learn how to do unit and integration testing in the Javascript land.

Why to Bother Implement Tests?

You should implement tests for that functionality that you develop and maintain. In the development phase, it is easier to implement the functionality if you have a test case for that functionality. In the maintenance phase, you can modify the source code with greater confidence if you have a good test suite that tells you that you didn’t break anything.

Unit tests typically test some behaviour in isolation, e.g. without integrations or a running web server.

Integration tests typically test the behaviour with some context like the web server.

Ad Hoc Bash Tests

If I develop some functionality and I need to integrate to some integration platform and to use its API, I often create a simple bash script to try that the API works as I expect it to work. In the following product-groups.sh we do exactly this:

#!/bin/bash

RET=$(http POST http://localhost:6600/login username=jarska password=joo Content-Type:application/json)
TOKEN=$(echo $RET | jq '.token' | tr -d '"') 
#echo $TOKEN

#http http://localhost:6600/product-groups
#http http://localhost:6600/product-groups x-token:"WRONG-TOKEN"
http http://localhost:6600/product-groups x-token:$TOKEN

This is kind of manual testing. In this script we call the /login API to get the token which we need to test the actual API, /product-groups. The commented lines are meant to test situations in which the token is missing or malformed.

You should not create these kind of tests for automated testing purposes. The next two chapters illustrate the automated unit and integration tests.

By the way. Postman is a great tool for manual API testing. Personally, I find it a bit awkward. The various security, header etc, sections are in different views, and you have to use the mouse to navigate and setup everything before you can hit your API with Postman. I rather use simple command line tools, like curl or httpie (the example above uses httpie) when my younger colleagues are laughing at a dinosaur like me and use more sophisticated tools, like Postman (and are clicking their precious mouses all day long). The beauty using simple command line tools is that you can glue them together using e.g. bash or babashka, and utilize other command line tools in processing, like jq in the example above.

Unit Tests

I use Vitest unit test framework, which is a natural choice with Vite.

Add the test script in your package.json:

  "scripts": {
    ...
    "test": "NODE_ENV=test vitest",
    ...

Then implement some tests. Example: domain.test.mjs.

test('Get product groups ok', async () => {
  const productGroups = await getProductGroups();
  expect(productGroups).be.an('array');
  expect(productGroups.length).toBe(2);
  expect(productGroups).toContainEqual({ pgId: 1, name: 'Books' });
  expect(productGroups).toContainEqual({ pgId: 2, name: 'Movies' });
});

Once you have some tests, start the test watcher in your terminal: pnpm test:

Unit test runner

Unit test runner in the terminal.

The test watcher runs the unit tests automatically when some source code file changes. This happens really fast, so you can leave one terminal open for test watcher.

Integration Tests

I used PactumJS for integration testing.

First create the test-integration script to trigger integration testing.

  "scripts": {
    ...
    "test-integration": "node ./node_modules/mocha/bin/mocha test-integration/**/*.{test,spec}.{js,mjs}"
    ...

Note, that I have the unit tests in the test folder, and integration tests in the test-integration folder. The unit test watches does not run the integration tests since these typically take longer, and you run them occasionally yourself during development. Your CI/CD pipeline runs both the unit and integration tests, of course.

Example: router.test.mjs .


// eslint-disable-next-line no-undef
it('Call /product-groups', async () => {
  const res = await axios.post(`${baseUrl}/login`, { username: 'jarska', password: 'joo' });
  const { token } = res.data;
  await spec()
    .get(`${baseUrl}/product-groups`)
    .withHeaders({ 'x-token': token })
    .expectStatus(200)
    .expectJsonMatch({ ret: 'ok', product_groups: [{ pgId: 1, name: 'Books' }, { pgId: 2, name: 'Movies' }] });
});

Run your integration tests:

Integration test runner

Integration test runner in the terminal.

Basically, this integration test runs the same test as my earlier ad hoc bash test, but with a good integration testing framework you have two benefits:

  1. It is easier to automate the integration testing, since your integration testing framework runner runs the tests and checks that they pass.
  2. The integration testing framework typically provides various out-of-the-box validation functionality to check the responses, e.g. .expectJsonMatch({ ret: 'ok', product_groups: [{ pgId: 1, name: 'Books' },....

Conclusions

Javascript has a huge ecosystem and there are various good tools for both unit and integration testing your source code.

The writer is working at a major international IT corporation building cloud infrastructures and implementing applications on top of those infrastructures.

Kari Marttila

Kari Marttila’s Home Page in LinkedIn: https://www.linkedin.com/in/karimarttila/