Logo
ManuelSchoebel

Jest unit and snapshot testing with TypeScript in a next.js app

Since I was asked, I will also throw Jest for testing into the stack composed of next.js and TypeScript. So let's have a look into how that can be achieved.

Add Jest

First we need to add Jest:

npm i -D jest @types/jest ts-jest

Next we need to update our package.json for jest configs and to also add the run script:

// package.json
{
  ...
  "scripts": {
    ...
    "test": "jest",
  }
  ...
  "jest": {
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js"
    ],
    "transform": {
      "^.+\\.tsx?$": "ts-jest"
    },
    "testMatch": [
      "**/*.(test|spec).(ts|tsx)"
    ],
    "globals": {
      "ts-jest": {
        "babelConfig": true,
        "tsConfig": "jest.tsconfig.json"
      }
    },
    "coveragePathIgnorePatterns": [
      "/node_modules/"
    ],
    "coverageReporters": [
      "json",
      "lcov",
      "text",
      "text-summary"
    ],
    "moduleNameMapper": {
      "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/mocks.js",
      "\\.(css|less)$": "<rootDir>/__mocks__/mocks.js"
    }
  }
}

As you can see in the jest config, we specified an additional tsconfig file. So we create this one next:

// jest.tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "esnext",
    "jsx": "react",
    "sourceMap": false,
    "experimentalDecorators": true,
    "noImplicitUseStrict": true,
    "removeComments": true,
    "moduleResolution": "node",
    "lib": ["es2017", "dom"],
    "typeRoots": ["node_modules/@types", "src/@types"]
  },
  "exclude": ["node_modules", "out", ".next"]
}

Running the test script with npm run test lets us know, that jest cannot find any test. Makes sense, since we have not yet create one. So let us add a very simple unit test. First we create a function that we want to test:

// components/Button.tsx
import * as React from 'react'
 
export function giveMeFive(): number {
  return 5
}
 
type Props = {
  buttonText: string
}
 
export default (props: Props) => (
  <button onClick={e => console.log(giveMeFive())}>{props.buttonText}</button>
)

We added a small function, that we also export so we can test it. Next we will add the jest test:

// components/__test__/button.tests.ts
import { giveMeFive } from '../Button'
 
test('generateAttributeIds', () => {
  expect(giveMeFive()).toBe(5)
})

When running the test it succeeds and jest runs with TypeScript.

Testrun with jest and TypeScript

Snapshot testing with jest and TypeScript

At first more packages... Yay!

npm i -D react-test-renderer

Next we can add a snapshot test (note that we create a tsx file this time):

// components/__tests__/button.snapshot.test.tsx
import * as React from 'react'
import Button from '../Button'
import renderer from 'react-test-renderer'
 
it('renders correctly', () => {
  const tree = renderer.create(<Button buttonText="Some Text" />).toJSON()
  expect(tree).toMatchSnapshot()
})

Running the test shows again everything is successful. Since it was the first time we were running the snapshot test, a snapshot is created. You can find them within the newly created __snapshots__ folder.

Snapshot folder

Now let us change the Button component just a little bit. And pretend it was an accident:

import * as React from 'react'
 
export function giveMeFive(): number {
  return 5
}
 
type Props = {
  buttonText: string
}
 
export default (props: Props) => (
  <button onClick={e => console.log(giveMeFive())}>{props.buttonText}!</button>
)

Do you see the difference? No? It is hard to find... But let us run the test once again:

Snapshot test failed

Now we can see there is a ! that was added. Since we really like it, we can now update the snapshot:

npm test -- -u

And run the test again with npm run test. We can see the snapshot was updated and the new testrun is successful again:

Snapshot test udpated

That is it for now, I hope it was useful :)

©️ 2024 Digitale Kumpel GmbH. All rights reserved.