Node.js (Express) with TypeScript, Eslint, Jest, Prettier and Husky - Part 3

oxodesign

Flamur Mavraj

Posted on August 27, 2020

Node.js (Express) with TypeScript, Eslint, Jest, Prettier and Husky - Part 3

Let's start testing!

Why you should do testing?

Before we continue with our setup I wanted to say some words regarding why you should do testing.

Yes testing is time consuming, and from time to time also hard but testing will save your ass in the long run.

Different testing techniques/tools exists and you should not (dont need to) cover all of them, also if use use multiple techniques/tools try to find a balance to not repeat your tests.

Finding the balance on what to test and what not may be hard, specially if you work with large teams, so my suggestion is to set up some rules that everyone follows, here are some rules we at Ornio AS try to embrace when it comes to testing Node.js applications:

  • All utility and service functions should be followed with a test, this should cover most of our functionality since we use Repository (service) pattern.
  • Validations (we use Joi) should also be tested.
  • Test Error handling.
  • Test Middlewares using Supertest.
  • Test critical Controllers using Supertest.

❓ What about you? What do you test in your Node.js applications?

Jest

Jest is a delightful JavaScript Testing Framework with a focus on simplicity.

Installing Jest is easy, run the command below and you will install Jest including its type definition and Typescript Jest runner:

npm i -D jest @types/jest ts-jest
Enter fullscreen mode Exit fullscreen mode

Then we need to do some configuration, by initiating Jest to generate its config file, run:

jest --init
Enter fullscreen mode Exit fullscreen mode

Answer the questions as needed, here is our answers:

Choose the test environment that will be used for testing
Node

Do you want Jest to add coverage reports?
No

Which provider should be used to instrument code for coverage?
Babel

Automatically clear mock calls and instances between every test?
Yes

This will generate a file called: jest.config.js:

module.exports = {
    clearMocks: true,
    moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
    roots: [
        "<rootDir>/src"
    ],
    testEnvironment: "node",
    transform: {
        '^.+\\.tsx?$': 'ts-jest'
    },
};
Enter fullscreen mode Exit fullscreen mode

We added some additional configuration in order to get jest and ts-jest running in our environment.

transform is setup to look for .ts and .tsx files and use ts-jest to run it.
moduleFileExtensions has also been updated with ts and tsx.

Alternative: Using preset to run ts-jest

Instead of configuring transform and moduleFileExtensions you can define a preset in your jest.config.js file.

ts-jest comes with 3 presets for you to use:

  • ts-jest/presets/default or ts-jest: Its the default preset. TypeScript files will be handled by ts-jest, leaving JavaScript files as-is.

  • ts-jest/presets/js-with-ts: Both TypeScript and JavaScript files will be handled by ts-jest.

  • ts-jest/presets/js-with-babel: TypeScript files will be handled by ts-jest and JS files will be handled by Babel

jest.config.js will look like this when using default preset:

module.exports = {
    clearMocks: true,
    roots: ['<rootDir>/src'],
    testEnvironment: 'node',
    preset: 'ts-jest'
};
Enter fullscreen mode Exit fullscreen mode

Then go ahead and add a test script in your package.json file:

"scripts": {
    //...
    "test": "jest"
},
Enter fullscreen mode Exit fullscreen mode

Since we don't have any logic in our app, we are going to create a utility function just for this purpose in order to to write a test for it, let's create something that checks if a parameter is and number. Create a file utils/isNumber.ts:

export const isNumber = (n: any) => {
    return !isNaN(parseFloat(n)) && isFinite(n);
}
Enter fullscreen mode Exit fullscreen mode

Now lets write a test for it, We prefer to add tests on the same place as our code, utils/isNumber.test.ts:

import {isNumber} from "./isNumber";

describe('isNumber Utils', () => {
    it('Its a number', () => {
        [0, 1, 2, -1, 1.345e17, '1'].map((n) => {
            expect(isNumber(n)).toEqual(true);
        });
    });

    it('Its not a number', () => {
        [false, true, NaN, [], {}, '1a'].map((n) => {
            expect(isNumber(n)).toEqual(false);
        });
    });
});
Enter fullscreen mode Exit fullscreen mode

describe is used to group tests and it defines a test. You can run multiple tests under it but try to keep each test as small as possible for better readability. On the other hand expect is a Jest function that is used to check a value and its used "always" with a matcher function, in our case toEqual

The test above will test different inputs towards our function and fail/pass based on its return.

When testing the golden rule is:

Cover all thinkable scenarios (not always possible) and create a test for it!

Jest is quite powerful so take a look into its documentation and explore it further more.

If you have any questions please do ask!

Supertest

With a fluent API, Supertest will help us test Node.js HTTP servers. Supertest is build upon Super-Agent Node.js library.

To install Supertest run:

npm i -D supertest @types/supertest
Enter fullscreen mode Exit fullscreen mode

In order to test our app, we are going to do some refactoring, create an file under src/app.ts:

import express, { Application } from 'express';
import {routes} from "./routes";

// Boot express
export const app: Application = express();

// Application routing
routes(app);
Enter fullscreen mode Exit fullscreen mode

Also we need/prefer to moved our routes definition into src/routes.ts:

import { Application, Router } from 'express';
import { PingController } from "./controllers/PingController";
import { IndexController } from "./controllers/IndexController";

const _routes: [string, Router][] = [
    ['/', IndexController],
    ['/ping', PingController],
];

export const routes = (app: Application) => {
    _routes.forEach((route) => {
        const [url, controller] = route;
        app.use(url, controller);
    });
};
Enter fullscreen mode Exit fullscreen mode

This way we can start organize our application easier. Go ahead and create following controllers, first, src/controllers/IndexController.ts:

import { NextFunction, Request, Response, Router } from 'express';
export const IndexController: Router = Router();

IndexController.get('/', async (req: Request, res: Response, next: NextFunction) => {
    try {
        res.status(200).send({ data: 'Hello from Ornio AS!' });
    } catch (e) {
        next(e);
    }
});
Enter fullscreen mode Exit fullscreen mode

And then src/controllers/PingController.ts:

import { NextFunction, Request, Response, Router } from 'express';
export const PingController: Router = Router();

PingController.get('/', async (req: Request, res: Response, next: NextFunction) => {
    try {
        res.status(200).send({ data: 'Pong!' });
    } catch (e) {
        next(e);
    }
});
Enter fullscreen mode Exit fullscreen mode

And finally our src/index.ts file is refactored to this:

import {app} from "./app";

const port = 5000;

// Start server
app.listen(port, () => console.log(`Server is listening on port ${port}!`));
Enter fullscreen mode Exit fullscreen mode

Now we can go ahead and create a test for testing our PingController using Supertest. src/controller/PingController.test.ts:

import request from 'supertest';
import { app } from '../app';

describe('Test PingController', () => {
  it('Request /ping should return Pong!', async () => {
    const result = await request(app).get('/ping').send();

    expect(result.status).toBe(200);
    expect(result.body.data).toBe('Pong!');
  });
});
Enter fullscreen mode Exit fullscreen mode

We start with a normal Jest test by describing it and then we call request(app) chained using ´get('/ping')´ which is the route and then finally we use send() to send the request.

When the request is sent and result is populated with the data we then checked if the status is 200 and the body.data is equal to Pong!.

Supertest is quite powerful and can be used to send advance requests by modifying headers, generating/storing tokens etc. It supports all CRUD operations.

I strongly recommend you to take a look on their documentation for more information and what Supertest can do.

Thats all for now. Until next time happy coding :)

Source code

You can find the source code here.

Need help?

Comment here or ping me on Twitter and I will gladly try to help you :)


Whats next?

  • Docerizing an Node.js/Typescript (this) application (Part 4)

💖 💪 🙅 🚩
oxodesign
Flamur Mavraj

Posted on August 27, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related