DEV Community

Kengo TODA
Kengo TODA

Posted on

How to replace @types/jest with @jest/globals and jest-mock

I believe that it's always better to prefer official type definitions rather than unofficial DefinitelyTyped. And Jest v27 introduced new defaults so I thought that it's a nice timing to replace @types/jest with @jest/globals and jest-mock. I'll share several problems I met:

Dependency Update

jest-circus is now default test runner, so we don't need to depend on it directly. Also remove @types/jest to replace with @jest/globals and jest-mock:

npm remove jest-circus @types/jest
npm add -D jest@^27.0.6 ts-jest@^27.0.5
Enter fullscreen mode Exit fullscreen mode

Configuration

I'm using ts-jest to transpile TypeScript code, and its configuration is like below. Now we can remove both testEnvironment and testRunner in most cases:

const { defaults: tsjPreset } = require('ts-jest/presets')

module.exports = {
  clearMocks: true,
  moduleFileExtensions: ['js', 'ts'],
  // no need `testEnvironment: "node"` any more
  testMatch: ['**/*.test.ts'],
  // no need `testRunner: "jest-circus"` any more
  transform: {
    ...tsjPreset.transform,
  },
  verbose: true
}
Enter fullscreen mode Exit fullscreen mode

I do not use preset to configure ts-jest, to let other integrations like puppeteer use it.

Import from @jest/globals and jest-mock

As described in the official API Document, we can import Jest API from @jest/globals module.

About SpyInstance, @types/jest provides it as jest.SpyInstance. But official type definition provides it as import { SpyInstance } from 'jest-mock'; ... with generics!

import { beforeEach, jest } from '@jest/globals';
import { SpyInstance } from 'jest-mock';

let spyOSHomedir: SpyInstance<string, []>;

beforeEach(() => {
  spyOSHomedir = jest.spyOn(os, 'homedir');
  spyOSHomedir.mockReturnValue(__dirname);
});
Enter fullscreen mode Exit fullscreen mode

This generics will bring better type safety, and tons of code change. For instance, mocked function sometimes wants to return the value that satisfies required type partially. In the following case, mocked fs.statSync returns an object that has only isFile() method even though the required type fs.Stats has more properties and methods. In such case we can use the Partial utility type:

import { beforeEach, jest } from '@jest/globals';
import { SpyInstance } from 'jest-mock';
import fs from 'fs';

let spyFsStat: SpyInstance<Partial<fs.Stats | fs.BigIntStats>, [fs.PathLike, fs.StatOptions?]>

beforeEach(() => {
  spyFsStat = jest.spyOn(fs, 'statSync');
  spyFsStat.mockImplementation((file: fs.PathLike) => {
    return { isFile: () => file === expectedJdkFile };
  });
});
Enter fullscreen mode Exit fullscreen mode

One more example: when you mock overloaded methods, you'll face some difficulty like:

https://javascript.plainenglish.io/mocking-ts-method-overloads-with-jest-e9c3d3f1ce0c

By applying the solution introduced in that article, we may lose integrity in test codes. So it would be an acceptable solution to use unknown in some cases:

let spyFsReadDir: SpyInstance<unknown, Parameters<typeof fs.readdirSync>>;

beforeEach(() => {
  // returned value is typed as `Dirent[]` but we want to return `string[]`, so use `unknown` to relax mismatch
  spyFsReadDir = jest.spyOn(fs, 'readdirSync');
  spyFsReadDir.mockImplementation(() => ['JavaTest']);
});
Enter fullscreen mode Exit fullscreen mode

That's all, you may refer to my commit replacing DefinitelyTyped with official type definitions. Thank you!

Discussion (0)