How Setting Up Unit Tests With TypeScript
By: Matteo Tafani Alunno
Tags: JavaScript, Mocha, Node, Nodejs, Typescript

./tests/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('./tests/tsconfig.json');



tsNode.register({

files: true,

transpileOnly: true,

project: './tests/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

| .nyrc.json

| package.json

| register.js

| tsconfig.json

./.mocharc.json

{

"require": "./register.js",

"reporter": "dot"

}

./.nyrc.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 'tests/**/*.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 'tests/**/*.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

| .nyrc.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('./tests/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.