How Setting Up Unit Tests With TypeScript

Matteo Tafani Alunno
The Startup
Published in
6 min readNov 6, 2020

Unit testing is one of the most valuable and tedious measures of your codebase at the same time. Writing tests is time-consuming, and it could be another sort of “sub-application” near to your main code. If correctly handled, unit tests may save your life and your mind when debugging code. We all know it!

This approach will help you to set up an opinionated unit testing environment with TypeScript that also works for JavaScript projects. You can create your next project following this approach, or you can adapt your existing app with a “quite-zero” effort. This approach is both for a client and a server app.

Setup

Let’s start with the project folder structure.

root
| node_modules
| src
| test
| package.json
| tsconfig.json

I considered a dedicated test folder in which you will organize your tests following the same internal structure of the src folder. It is not strictly required. It also works if you want to maintain a common approach and keeping the tests in the component folder for the client-side projects. It will work as well.

As usual, for JavaScript/TypeScript projects, we need some dependencies.

npm install --dev ts-node mocha @testdeck/mocha nyc chai ts-mockito

or

yarn add -D ts-node mocha @testdeck/mocha nyc chai ts-mockito

What we’ll get:

  • ts-node: allow to run .ts file by transpiling them on the fly.
  • mocha: we’ll use it as the test runner.
  • @testdeck/mocha: it will allow mocha to interpret TypeScript classes as test suites.
  • nyc: it will generate the coverage report.
  • chai: the expectation library we’ll use.
  • ts-mockito: a supercool stubbing and mocking library inspired by mockito for Java.

Well, the first thing to do is to configure the tsconfig.json. I suppose you have a root tsconfig for your project, whatever it is. Based on the main one, you can create a tsconfig.json for tests inside the test folder.

| node_modules
| src
| test
| --- tsconfig.json
| package.json
| tsconfig.json

./test/tsconfig.json

{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"module": "commonjs",
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"isolatedModules": false,
"strict": false,
"noImplicitAny": false,
"typeRoots" : [
"../node_modules/@types"
]
},
"exclude": [
"../node_modules"
],
"include": [
"./**/*.ts"
]
}

If you are familiar with TypeScript, the configuration above is nothing special. The main important things to preserve are the module and the experimentalDecorators properties. Apart from that, you can change it as you need.

Then, we need to configure a file in the root folder that allows ts-node to run and transpile the tests:

| node_modules
| src
| test
| --- tsconfig.json
| package.json
| register.js
| tsconfig.json

./register.js

/**
* Overrides the tsconfig used for the app.
* In the test environment we need some tweaks.
*/

const tsNode = require('ts-node');
const testTSConfig = require('./test/tsconfig.json');

tsNode.register({
files: true,
transpileOnly: true,
project: './test/tsconfig.json'
});

The register file loads the tsconfig to instrument the ts-node. It improves performance with a negligible compromise. The transpileOnly property tells TypeScript to avoid checking the test code during the “compiling” phase. Overall, it is not risky, just test code and not the production one. The result is that you will catch the transpiling errors when the tests run instead of at the build-time. Anyway, this little trick allows specifying the properties just for the ts-node, so your IDE will continue to use the original tsconfig to check the test files during the code writing. Not bad.

The last two files we need to add to the project are:

| node_modules
| src
| test
| --- tsconfig.json
| .mocharc.json
| .nycrc.json

| package.json
| register.js
| tsconfig.json

./.mocharc.json

{
"require": "./register.js",
"reporter": "dot"
}

./.nycrc.json

{
"extends": "@istanbuljs/nyc-config-typescript",
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules/"
],
"extension": [
".ts"
],
"reporter": [
"text-summary",
"html"
],
"report-dir": "./coverage"
}

Well done. These two files allow mocha to be aware of the register.js we just created and nyc to generate the HTML coverage inside the ./coverage folder. Try to experiment with the options to produce different results.

We are at the final setup step. We want to run the tests. So, open your package.json and add the following entry in the scripts object:

"test": "nyc ./node_modules/.bin/_mocha 'test/**/*.ts'",

As a best practice, I usually add a suffix to my test files. This allows me to distinguish the files I want to run with mocha from the utilities I might create during the project lifecycle. Given that, the previous command would be:

"test": "nyc ./node_modules/.bin/_mocha 'test/**/*.test.ts'",

Testing

Now that we have the complete testing environment set up, we need to start testing the code.

Let’s create our hello world test.

./test/hello-world-service.unit.test.ts

import { suite, test } from '@testdeck/mocha';
import * as _chai from 'chai';
import { mock, instance } from 'ts-mockito';
import { HelloWorldService } from '../src/logger/hello-world.service.ts';
import { Logger } from '../src/logger/logger.ts';
_chai.should();@suite class HelloWorldServiceUnitTests {

private SUT: HelloWorldService;
private loggerMock: Logger;

before() {
this.loggerMock = mock(Logger);
this.SUT = new HelloWorldService(instance(this.loggerMock));
}

@test 'should do something when call a method'() {
this.SUT.should.be.not.undefined;
}

}

It is the generic structure of a test. You can use TypeScript classes with suite decorator to mark classes as a suite and test decorator to make the methods runnable tests. As you can see, I defined the methods as strings. It improves readability, but if you prefer, you can use traditional methods.

Given the script we just set up, you can run the test with:

npm test

or

yarn test

Tip: avoid repetitive code

In order to set up chai, import the decorators and, doing some other little code setup, you have to copy and paste some lines from a test to another. Avoiding it could be achieved by using the TypeScript paths.

Let’s create a new folder utility inside the test folder:

| node_modules
| src
| test
| --- utility
| --- tsconfig.json
| .mocharc.json
| .nycrc.json
| package.json
| register.js
| tsconfig.json

Then, let’s add an index.ts inside it with this code:

export { suite, test, params, skip, only } from '@testdeck/mocha';

import * as _chai from 'chai';
const _should = _chai.should();
export const should = _should;

Now, edit the tsconfig.json inside the test folder to add the paths section:

"paths": {
"@my-org/my-project/test": [
"./utility/index.ts"
]
}

In order to make it work with ts-node, we need a new package:

yarn add -D tsconfig-paths

Also, edit the register.js and change the code with this:

const tsNode = require('ts-node');
const tsConfigPaths = require('tsconfig-paths');
const mainTSConfig = require('./tsconfig.json');
const testTSConfig = require('./test/tsconfig.json');

tsConfigPaths.register({
baseUrl: './test',
paths: {
...mainTSConfig.compilerOptions.paths,
...testTSConfig.compilerOptions.paths
}
});

tsNode.register({
files: true,
transpileOnly: true,
project: './test/tsconfig.json'
});

Cool. So, now you can change your import statement in the tests:

import { suite, test, should } from '@my-org/my-project/test';
import { mock, instance } from 'ts-mockito';
import { HelloWorldService } from '../src/logger/hello-world.service.ts';
import { Logger } from '../src/logger/logger.ts';
_chai.should();@suite class HelloWorldServiceUnitTests {

private SUT: HelloWorldService;
private loggerMock: Logger;

before() {
this.loggerMock = mock(Logger);
this.SUT = new HelloWorldService(instance(this.loggerMock));
}

@test 'should do something when call a method'() {
this.SUT.should.be.not.undefined;
}

}

Finally, you can just use a single import to get all the utility libraries at once.

Tip: setting up WebStorm/PhpStorm

I usually set up my IDE (PhpStorm) to run the tests by using the little play icon that it displays near the method definition. When I write down the tests, it is helpful since it also allows me to debug the code.

Setting up *Strom IDE to run the tests with this configuration is easy. You need to go to Edit Configuration and expand the Templates on the left. Just select the Mocha template and elect a setup like this:

Now, when you run the tests using the play icon near the test method, it will use this template to create the run configuration for that method on the fly.

--

--