Node.js (Express) with TypeScript, Eslint, Jest, Prettier and Husky - Part 3
Flamur Mavraj
Posted on August 27, 2020
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
Then we need to do some configuration, by initiating Jest to generate its config file, run:
jest --init
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'
},
};
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
orts-jest
: Its the default preset. TypeScript files will be handled byts-jest
, leaving JavaScript files as-is.ts-jest/presets/js-with-ts
: Both TypeScript and JavaScript files will be handled byts-jest
.ts-jest/presets/js-with-babel
: TypeScript files will be handled byts-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'
};
Then go ahead and add a test script in your package.json
file:
"scripts": {
//...
"test": "jest"
},
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);
}
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);
});
});
});
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
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);
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);
});
};
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);
}
});
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);
}
});
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}!`));
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!');
});
});
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)
Posted on August 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.