JestでTypeScriptを高速化する

Jestでテストの高速化させる方法を紹介します。トランスフォーマーとしてesbuildやswcを紹介し、TypeScriptで遅くなりがちなトランスパイルを高速化させることで、テストを自体を高速化します。

2021/10/103 min read
..
hero image

はじめに

esbuild の登場により、フロントエンドの世界は、開発環境により速度を求めるようになりました。vite の隆盛はその最たるものといってもいいでしょう。

esbuildswc は高速な GoRust によって書かれ、更に多くの場合、Typescript の型チェックを省略しています。 tsc の型チェックは、大抵 IDE やワークフローで行われているので、これらを削ぎ落とすことで、純粋なコンパイラとして JavaScript への変換に特化しているということですね。

さて、Typescript コードをテストする際、多くの場合ts-jestbabel-jestをトランスフォーマーとして使用していると思います。しかし、これらによってテストの速度が低下することがあります。

今回は jest の実行を高速化し、高速なテストを実現する方法を紹介します。

結論

先に導入方法について書いておきます。

1
yarn add -D jest @swc/jest
bash
1
2
3
4
5
{
"transform": {
"^.+\\.(t|j)sx?$": "@swc/jest"
}
}
jest.config.jsonjson

トランスフォーマーに swc を jest 向けに調整した @swc/jest を使います。

パフォーマンス比較

高速化によって、どの程度パフォーマンスが改善したか見てみます。

実行環境によってパフォーマンスは異なるので、実測値ではなくそれぞれ結果の相対的な対比を行います。

CommonJS + Javascript

理論上最速となりそうなパターンを試してみます。CommonJS 形式の JavaScript はトランスパイルの必要がないはずなので、最速になるはずです。(間違ってたらごめんなさい 🙏)

関数の中身に興味はないので、適当な関数を用意してテストします。

1
exports.add = (a, b) => a + b
index.jsjs
1
2
3
4
5
6
7
8
const { add } = require('../src')
describe('add', () => {
it('should return 2 when it gives 1,1', () => {
const result = add(1,1)
expect(result).toBe(2)
})
})
test/index.spec.jsjs
1
2
3
4
module.exports = {
testEnvironment: "node",
roots: ["<rootDir>/test/"]
};
jest.config.jsjs

これをキャッシュを無効にして 10 回程度の平均を取ります。 あまり厳密な測定ではないですが、今回はそれぞれの速度比較なので、条件を合わせることで相対的な比較はできていると考えます。

1
for i in {0..9}; do yarn jest --no-cache ; done
bash

結果:

Transformer平均値(s)
なし(CommonJS + JavaScript)0.512

これを基準に考えていきます。

ESM + TypeScript

TypeScript を ES module 形式で記述するパターンです。TypeScript を使う場合の多くはこのパターンでしょう。

1
export const add = (a: number, b: number): number => a + b
index.tsts
1
2
3
4
5
6
7
8
import { add } from '../src/'
describe('add', () => {
it('should return 2 when it gives 1,1', () => {
const result = add(1,1)
expect(result).toBe(2)
})
})
test/index.spec.tsts

トランスフォーマーに ts-jest を使う

1
2
3
4
5
6
7
module.exports = {
...,
transform: {
'^.+\\.tsx?$': 'ts-jest',
}
};
jest.config.jsjs

結果:

Transformer平均値(s)
ts-jest1.660
なし(CommonJS + JavaScript)0.512

CommonJS + JavaScript と比較すると3倍程度時間がかかっています。これはなんとかしたいですね。

トランスフォーマーに esbuild を使う

esbuildは Go で記述された高速なバンドラーです。バンドラーと言ってもデフォルトで、TypeScript 構文の解析と型注釈の破棄に対するサポートが組み込まれています。

1
yarn add -D esbuild-jest esbuild
bash
1
2
3
4
5
6
module.exports = {
...,
transform: {
'^.+\\.tsx?$': 'esbuild-jest'
}
};
jest.config.jsjs

結果:

Transformer平均値(s)
esbuild-jest0.373
ts-jest1.660
なし(CommonJS + JavaScript)0.512

驚異的な速度ですね。特に CommonJS + JavaScript よりも早いのは驚きですね。

トランスフォーマーに swc を使う

swc は、rust で記述された超高速コンパイラです。 Denodeno lintdeno docに使っているみたいですね。

1
yarn add -D @swc/jest
bash
1
2
3
4
5
6
module.exports = {
...,
transform: {
'^.+\\.tsx?$': ['@swc/jest'],
}
};
jest.config.jsjs

結果:

Transformer平均値(s)
@swc/jest0.351
esbuild-jest0.373
ts-jest1.660
なし(CommonJS + JavaScript0.512

esbuildswc も驚異的な速度でトランスパイルできることがわかります。双方の速度面の比較はこの結果だけではできませんが、調べた感じだとswcのほうが若干有利のようでした。ただし、esbuild-jest ではオプションとして次の項目を変更できる利点があります。

1
2
3
4
5
6
7
8
9
10
interface Options {
jsxFactory?: string
jsxFragment?: string
sourcemap?: boolean | 'inline' | 'external'
loaders?: {
[ext: string]: Loader
}
target?: string
format?: string
}
ts

また、VSCode の場合は、jest の拡張機能が提供されています。 これは、テスト対象に変更があった場合などに、バックグラウンドで自動的にテストを実行してくれますが、当然このテストも高速になります。

テストが成功した場合、✅を対象コードにつけてくれます。テストがすぐに終わるため、すぐにマークをつけてくれるのが開発体験としてはかなりいいです。

jest.config.ts をやめる

上の結果は、jestの設定ファイルとしてjest.config.jsを使っていました。 しかし、.ts形式の設定ファイルにすると、トランスフォーマーを変えたとしても、パフォーマンスがあまり改善しません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// @swc/jest + jest.config.js
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.385 s
Ran all test suites.
✨ Done in 1.24s.
// @swc/jest + jest.config.ts
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.389 s
Ran all test suites.
✨ Done in 2.93s.
bash

ファイル形式を .ts に変えただけで2倍以上時間がかかるようになってしまいました。

これは jest は jest.config.ts のトランスパイルに ts-node を要求しているからです。 そこで可能であれば json 形式で jest.config.json や諦めて jest.config.js 形式で設定ファイルを書く必要があります。

Cons

冒頭述べている通り、esbuildswc は型チェックを省略し、速度を享受しています。よって、次のコードはテスト時にコンパイルエラーを検出できません。

1
export const add = (a: number, b: string): number => a + b
ts

この場合は、アノテーションが不適切ですが、JavaScript にコンパイルされたときには、正常のコードとして動くのでテストも通ります。

かといっても、大抵の IDE ではエラーを視覚的に表示しているはずですし、ワークフローに tsc を追加しておけば、未然に防ぐことができます。

以上のことから、速度重視で代替手段や工夫が講じられる環境であれば、積極的に採用できるのではと思います。


Edit this page on GitHub

Other Article

Comments