Setting up Unit Tests with TypeScript is sometimes less than intuitive. My aim here is to present the pros and cons of using Typescript and to detail how simple it is to set up typescript unit tests with Jest and Mocha.
- Avoid hidden errors such as
'undefined' is not a function
- Easier to refactor code
- Self-documenting code
- Transpiles into the various releases of Javascript and it's common runtime environments.
- A study published in 2017 found that TypeScript projects produced 15% fewer bugs on average.
In my short time as a developer I've identified a few pitfalls of TypeScript.
- JS makes no assumptions about types which forces developers to write type checks.
- Adds complexity to the code which may make it less readable.
You might be asking right now, "Why would I use Typescript for unit testing?"
To that I ask, what is more detrimental to customers:
- A product that takes longer to release because testing caught errors?
- Or a product that doesn't work because the tests didn't catch the errors?
If using Typescript helps developers to make fewer errors in their source code, wouldn't the same logic apply to our unit tests?
Regardless if your toolset includes TypeScript, being consistent and consolidating around the same language or process will speed up the development process.
- Install dependencies
npm i -D typescript @types/node @tsconfig/node14 del-cli
- Add tsconfig that extends @tsconfig/node14/tsconfig.json
{
"extends": "@tsconfig/node14/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": [
"src/**/*"
]
}
- Add clean and build scripts to package.json file
{
"scripts": {
"build": "npm run clean && tsc",
"clean": "del-cli dist"
}
}
- In the terminal, run
npm run build
to make sure the build step works. You should see adist
directory appear at your project root.
Installation and setup is straight-forward and simple for the majority of use-cases.
- Install dependencies
npm i -D jest ts-jest @types/jest
- Add jest configuration to the package.json file
{
"jest": {
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
},
"coverageThreshold": {
"branches": 80,
"lines": 80,
"functions": 80,
"statements": 80
}
}
}
- Add test and coverage scripts to the package.json file
{
"scripts:": {
"coverage": "jest --coverage || exit 0",
"test": "jest"
}
}
-
Add tests to the test directory (see TS-Jest Example)
-
In your terminal, run
npm test
ornpm coverage
. You should see acoverage
directory appear at your project root, if your tests completed.
import {add} from '../src'
describe('add', () => {
it('should add a single number', () => {
expect(add(1)).toEqual(1)
})
it('should add multiple numbers together', () => {
expect(add(1,2,3,4,5)).toEqual(15)
})
it('should add negative numbers together', () => {
expect(add(-1,-2,-3,-4,-5)).toEqual(-15)
})
it('should throw a type error if an argument isn\'t a number', () => {
expect(() => add(null)).toThrow(TypeError)
})
})
Mocha test dependencies are much more spread out so there's more to install. That's not to say that it's significantly more complicated to set up, but it may be slightly more cumbersome to keep dependencies up-to-date.
- Install dependencies
npm i -D mocha ts-mocha @types/mocha chai @types/chai nyc
Additionally, sinon
may be installed to add spies and mock functions.
- Add test and coverage scripts to the package.json file
{
"scripts": {
"coverage": "nyc npm test || exit 0",
"test": "ts-mocha test/**/*.ts"
}
}
- Add the follow nyc configuration file
nyc.config.js
to the root of your project
'use strict'
const {join} = require('path')
const {tmpdir} = require('os')
const {realpathSync} = require('fs')
const getCacheDirectory = () => {
const {getuid} = process
const tmpdirPath = join(realpathSync(tmpdir()), 'jest')
if (getuid == null) {
return tmpdirPath
}
// On some platforms tmpdir() is `/tmp`, causing conflicts between different
// users and permission issues. Adding an additional subdivision by UID can
// help.
return `${tmpdirPath}_${getuid.call(process).toString(36)}`
}
module.exports = {
'reporter': ['json', 'lcov', 'text', 'clover'],
'temp-dir': getCacheDirectory(),
'check-coverage': true,
'branches': 80,
'lines': 80,
'functions': 80,
'statements': 80
}
-
Add tests to the test directory (see TS-Mocha Example)
-
In your terminal, run
npm test
ornpm coverage
. You should see acoverage
directory appear at your project root, if your tests completed.
import {expect} from 'chai'
import {add} from '../src'
describe('add', () => {
it('should add a single number', () => {
expect(add(1)).to.equal(1)
})
it('should add multiple numbers together', () => {
expect(add(1,2,3,4,5)).to.equal(15)
})
it('should add negative numbers together', () => {
expect(add(-1,-2,-3,-4,-5)).to.equal(-15)
})
it('should throw a type error if an argument isn\'t a number', () => {
expect(() => add(null)).to.throw(TypeError)
})
})
When stubbing functions, classes, or objects in TypeScript, you'll have to make use of type aliasing and overwrites to not end up stubbing everything