Testing TypeScript with Jest

This is the fourth entry in a short series about creating React/Redux applications with TypeScript. Part one introduced a simple counter application, which we then dressed up with a simple React UI and API interactions. If you’re in a hurry, skip on over to the finished project on Github; everyone else, read on!

Followers of this blog won’t need to be convinced of the value of a well-maintained test suite. And lucky for us, Jest makes it nearly painless to test React applications–even with TypeScript in the mix. Let’s look at the layers involved in developing clean, test-secured code.

Linting

In vanilla JavaScript, linting validates syntax as a first line of defense against runtime errors. In TypeScript, static type-checking serves a similar purpose, though with a more sophisticated understanding of the raw syntax’s context. But linters have a secondary purpose: ensuring maintainability. Even with a type-system handling the heavy lifting of static analysis, we still benefit from linting for legible, consistent style.

Fortunately for us, tslint is a first-rate TypeScript linter with configuration and use familiar from JavaScript tools like jshint or eslint. After installing tslint, and initializing a default configuration, we’re ready to lint a new or existing project:

$ npm install -g tslint
$ tslint --init
$ tslint -c tslint.json 'src/**/*.{ts,tsx}'

Setting up Jest

Jest is a low-configuration testing harness popular in React applications, and for good reason: in vanilla JavaScript, it mostly Just Works™. That’s a big deal for developers used to wrestling with PhantomJS (you know who you are–and if you don’t, just use Jest).

Using Jest with TypeScript takes a little more effort. Facebook’s Jest/TypeScript example outlines the strategy: set up a preprocessor for any TypeScript files; run Jest as before. ts-jest is a great option for preprocessing, providing compilation, source-mapping, and an all-around pleasant preprocessing experience. It’s available via npm:

$ npm install --save-dev ts-jest

Now, to start testing we need only add a new top-level entry for "jest" into the project’s package.json:

"jest": {
  "moduleFileExtensions": [
    "ts",
    "tsx",
    "js"
  ],
  "transform": {
    "\\.(ts|tsx)$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
  },
  "testRegex": "/__tests__/.*\\.(ts|tsx|js)$"
}

While package.json is open, let’s add scripts for linting and testing to see all of our hard work in action:

"scripts": {
  "lint": "tslint -c tslint.json 'src/**/*.{ts,tsx}'",
  "pretest": "npm run lint",
  "test": "jest"
},

Add some tests

At this point, our tests are simply JavaScript. Let’s write a simple spec that will verify future changes of our <Counter /> component using a snapshot made possible, as all good things are, by Jest.

// src/components/__tests__/counter_spec.tsx
import * as React from 'react'
import * as TestUtils from 'react-addons-test-utils'

import { createStore } from 'redux'

import { Counter } from '../counter'
import { reducers } from '../../reducers'

describe('<Counter />', () => {
  it('renders', () => {
    const store = createStore(reducers)
    expect(TestUtils.createRenderer().render(
      <Counter label='a counter!' store={store} />
    )).toMatchSnapshot()
  })
});

By rendering the component with a shallow renderer, we’re able to compare to a snapshot exactly as we would in JavaScript. All that’s left is to watch it turn green.

$ npm test

Testing reducers

Redux reducers are meant to be easy to test. Using the same tools as with the component, we can verify reducer behavior by replaying a sequence of actions and comparing the resulting state:

// src/reducers/__tests__/index_spec.tsx
import reducers, {
  initialState,
} from '../index'

import {
  incrementCounter
} from '../../actions'

const resultOf = actions =>
  actions.reduce(reducers, initialState)

describe('reducers/counter', () => {
  it('starts at 0', () =>
    expect(resultOf([])).toMatchSnapshot())

  it('increments to 6', () =>
    expect(resultOf([
      incrementCounter(1),
      incrementCounter(2),
      incrementCounter(3),
    ])).toMatchSnapshot())
})

Conclusion

There you have it: the basic tools, configurations, and scripts to make testing a central part of a TypeScript application. We don’t have to stop here! Though it’s an exercise for another day, the same techniques we use to validate code and write unit tests can just as easily apply in end-to-end testing.

The completed counter project is available for reference on github, tests and all. Have an comment, suggestion, or improvement? The issue queue is open for business. And I’m looking forward to your suggestions, experiences, and feedback over on twitter.

And as ever, happy testing!

Up next: We’ll use the redux-thunk middleware to tighten down the counter’s API interactions. Read on!

Let’s keep in touch

Reach out on Twitter or subscribe for (very) occasional updates.

Hey, I'm RJ: digital entomologist and intermittent micropoet, writing from the beautiful Rose City.