jestとは?
- 近年最もポピュラーなJavaScriptテストフレームワークです。
- facebookが作っておりReactにも対応しています。
- 元々はFacebookの社内のみで使われいましたが2013年にオープンソース化されました。
Jestの特徴
ゼロコンフィグ
細かい設定を行うことなく動作する。
スナップショット
大きなオブジェクトの追跡するテストが簡単に書ける。複雑なオブジェクトやUIの状態をテストするのは基本難しいがこの機能を使えば簡単にテストできる。
一度スナップショットを取得すれば以降の変更を追跡、確認ができる。
独立的
独立して複数のテストを並列実行して、パフォーマンスを向上できる。
優れたAPI
テストツールがAPIとして整理されている。
注意点
ES6には対応していないのでbabelと一緒に導入する必要がある。
実際のブラウザでのテストを用いたテストはできない。
jsdomというnode上で動作する仮想的なブラウザを利用することでブラウザ環境をシミュレートしますが、実際のブラウザ上でのテストはサポートしていません。なので特定のブラウザでのみ発生するようなバグの検知はできないです。
他のフレームワークとの比較
2018年ごろまではこれらのフレームワークとGithubスター数は大差なかったが、2018年以降からJestが急上昇しており、これらのフレームワークの2倍程度のスター数になっている。
Mocha
- テストフレームワークとしては最小限の機能を提供
- その他ライブラリと組み合わせることで柔軟な設定が可能だが、設定が複雑になることが多い。
jasmine
- Angularの標準テストフレームワーク
- 基本機能な備わっているが、Jestに比べて一部高度な機能(スナップショット機能など)が備わっていない。
- 実行速度が遅い。
インストール
typescriptの場合
1 |
npm i -D jest @types/jest ts-jest |
typescriptのコードをテストする場合はts-jestというパッケージが必要です。
設定
typescriptの場合
1 |
npx ts-jest config:init |
上記コマンドを実行すると「jest.config.js」というファイルが自動生成されます。
jest.config.js
1 2 3 4 5 |
/** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { preset: 'ts-jest', testEnvironment: 'node', }; |
preset
一連の設定をまとめたものです。ts-jestであればTypeScriptをテストするための一連の設定が含まれています。
testEnvironment
Jestがどの環境でテストを実行するかを指定します。nodeが指定されていればnode-js環境でテストを実行するという意味になります。
テストコードを配置場所
「__tests__」というディレクトリにテストコードを配置することを推奨しています。
テスト実行(テストランナーとしての機能)
全てのテストを実行
package.jsonのscriptに定義しておくのが普通です。
1 |
jest |
or
1 |
npm test |
特定のファイルを指定して実行(個別実行)
1 |
jest ファイルのフルパス |
ファイル名を指定して実行
フルパスを指定する場合と違い、ファイル名に合致するファイルがあれば全て実行します。
1 |
jest ファイル名 |
テスト結果
Test Suites
成功したテストファイルの数を示しています。
Tests
テスト関数もしくはitの数の合計を示しています。
SnapShots
Jestのスナップショットテストを使った合計数です。
Time
テスト全体の実行時間を示しています。
テストケースを追加する機能
1 |
test case |
アプリケーションの期待値と結果を判定する機能
1 |
expect |
テストコードのデバッグ方法
テストコード内にconsole.logを埋め込んでテストを実行すればコンソール上にテストコード内の変数が出力されます。
1 |
console.log(変数); |
セットアップと破棄
describeを入れ子にして入れ子になったなかにそれぞれ仕込むことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
describe("outer", () => { beforeEach(() => { console.log("outer beforeEach"); }); afterEach(() => { console.log("outer afterEach"); }); beforeAll(() => { console.log("outer beforeAll"); }); afterAll(() => { console.log("outer afterAll"); }); it("outer test 1", () => { console.log("outer test 1"); }); describe("inner", () => { beforeEach(() => { console.log("inner beforeEach"); }); afterEach(() => { console.log("inner afterEach"); }); beforeAll(() => { console.log("inner beforeAll"); }); afterAll(() => { console.log("inner afterAll"); }); // このitではbeforeEachは外側と内側で二回実行される。 it("inner test 1", () => { console.log("inner test 1"); }); }); }); |
beforeEach
各テスト(it)の前に実行される。なお、describeを入れ子にした場合はこれだけは内側のdescribeの際も外側のdescribeの内容も二重に実行されることになるので注意です。
afterEach
各テスト(it)の後に実行される。
beforeAll
ファイル内の全てのテストの前に1度だけ実行される。
afterEach
ファイル内の全てのテストの後に1度だけ実行される。
モックとスパイの違い
モックとスパイの違いは以下になります。ただ、Jestではモックとスパイの機能が一部オーバーラップしています。
モック
- テストの事前段階で模倣する振る舞いを定義する。
- テスト実行時に実際の振る舞いとモックの振る舞いを置き換える。
スパイ
スパイとは、既存の関数やメソッドの呼び出しを「監視」するもの。スパイの対象が「いつ・何回・どんな引数で」呼び出されたかを検証できる。
- 実際の振る舞いを監視します。
- テスト実行時には実際の振る舞いを置き換えることもできますが、置き換えないです。
モック化
1 2 |
const mockFunc = jest.fn(); expect(mockFunc("あ")).toBe(undefined); |
ちなみに、モック関数(jest.fn)の戻り値は一律でundefinedになるので上記の実行結果はOKになります。
モック関数の行わせたい処理内容を設定する。
1 |
jest.fn(() => "い"); |
もしくは
1 |
jest.fn().mockImplementation(() => "い"); |
モック関数の処理内容を変更できます。(デフォルトはundefinedのため)
mockImplementationを使うことで後から実装を変更することができるというメリットがあります。
モック関数の戻り値を変更する。
mockReturnValue
何度実行しても同じ値を返します。
1 2 3 |
const mockFunc = jest.fn().mockReturnValue("戻り値") expect(mockFunc()).toBe("戻り値") // 成功する。 expect(mockFunc()).toBe("戻り値") // 成功する。 |
mockReturnValueOnce
Onceを付けると一度だけしか戻り値を返さない設定になります。
1 2 3 |
const mockFunc = jest.fn().mockReturnValueOnce("戻り値") expect(mockFunc()).toBe("戻り値") // 成功する。 expect(mockFunc()).toBe("戻り値") // エラーになる。二度目の呼び出しの戻り値はundefinedになります。 |
mockResolvedValue
非同期の戻り値を取得できます。
1 2 3 4 5 |
it("非同期", async () => { const mockFunc = jest.fn().mockResolvedValue("非同期の戻り値") const result = await mockFunc(); expect(result).toBe("非同期の戻り値") // 成功する。 }) |
存在する関数をモック化する。(spyOn)
1 |
const spy = jest.spyOn(Math, "abs").mockImplementation(() => 1); |
spyOnを使えば上記のように存在する関数(Math.abs)もモック化することができます。
また、以下のようにspyOnで監視したclassやメソッドに対して結果をテストをすることも可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
export class Calculator { sum(a: number, b: number): number { return a + b; } } it("spyon",()=>{ const calc = new Calculator(); const sumSpy = jest.spyOn(calc,"sum"); // spyがオリジナルの関数に憑依するイメージ calc.sum(2,3); expect(sumSpy).toHaveBeenCalledTimes(1); expect(sumSpy).toHaveBeenCalledWith(2,3); // 生成したspyが他のテストに影響を与えないようにspyを解除する(オリジナルの関数に戻す)ことが推奨されます。 // もし、テストケースが多い場合はafterEachを利用して自動的に毎テストコードで解除します。 sumSpy.mockRestore(); }) |
jest.fnとspyOnの使い分け
jest.fnのユースケース
- 単純に何らかの非同期処理の戻り値として適当なコールバック関数を定義しておきたい場合
spyOnのユースケース
何かしらのモジュールの特定関数だけ置き換えたい場合はjest.fnよりspyOnで明示的に定義した方が分かりやすいです。具体的には以下のようなケースで使います。
- ローカルストレージのメソッドremoveItemが呼ばれていることを回数チェックする。
- APIのrequestが呼ばれていることをチェックする。
モジュール全体のモック化
jest.mock
describeやitの中ではなくトップレベルで記述する必要があります。まだパスを渡しただけの初期状態では全ての関数、メソッドは実装を持たないモックになります。
1 2 3 4 |
import fs from "fs"; jest.mock("fs"); // モジュールのパスを引数に指定する。この時点でfsモジュールはモック化される。 const mockFs = jest.mocked(fs);// TSだとfsがモジュール化されていると認識してくれないのでmockedでtsに対応するためにラップする。 mockFs.readFileSync.mockReturnValue("戻り値") |
mockClear、mockRestore、mockReset
mockClear
mockFn.mock.calls(モックが呼ばれた回数)、mockFn.mock.instancesを初期化する。
mockRestore
jest.spyOnによって作成されたモックをオリジナルのものへ戻す。
mockReset
mockFn.mock、mockFn.mock.calls、mockFn.mock.instancesを初期化する。また、mockImplementation、mockReturnValueで設定した実装、戻り値も初期化
マッチャー
.not
検証を否定するマッチャーです。
1 |
expect(1+2).not.toBe(4); |
toBe
string、number同士を比較する場合はこちらでも良い。仕組み的にはJavaScriptのObject.is(===)と同等の比較が行われています。なので、オブジェクト同士の比較はtoEqualじゃないとできない。
1 |
expect(1+2).toBe(3); |
toEqual
オブジェクト同士を比較する場合はこちらを使う。
1 |
expect([1,2,3]).toEqual([1,2,3]); |
toStrictEqual
継承元を含む厳密な比較をする。ES6以降で導入されたclass構文などを使用している場合には使えそうです。(あまりフロントエンド開発で使う機会はないかもしれません。)
toBeNull
nullになるか検証する。
toBeTruthy
trueになるか検証する。
toBeFalsy
falseになるか検証する。
toMatch
正規表現でマッチするかチェックする。
toContain
配列やオブジェクトの中に値が含まれているか検証する。
toThrow
例外をthrowするかもテスト可能です。
toHaveBeenCalled
モック関数が実行されたかテストできます。注意点としては、普通の関数だと呼び出しに失敗します。モック関数かスパイじゃないとダメです。
1 2 3 4 5 |
it("モック関数が呼び出されるか",()=>{ const mockFunc = jest.fn(); mockFunc(); expect(mockFunc).toHaveBeenCalled(); }) |
toHaveBeenCalledtimes
モック関数が呼ばれる回数をテストできます。
1 2 3 4 5 6 |
it("モック関数が2回呼び出される。",()=>{ const mockFunc = jest.fn(); mockFunc(); mockFunc(); expect(mockFunc).toHaveBeenCalledTimes(2); }) |
toHaveBeenCalledWith
モック関数に渡される引数をチェックできます。
1 2 3 4 5 |
it("モック関数に渡される引数チェック",()=>{ const mockFunc = jest.fn(); mockFunc("あああ"); expect(mockFunc).toHaveBeenCalledWith("あああ"); }) |
テスト対象関数
1 2 3 4 |
export function kansu(a: number, b: number) { if (b === 0) throw new Error("0で割ることはできません"); return a/ b; } |
テストコード
1 |
expect(()=>divide(10,0)).toThrow("0で割ることはできません") |
パラメタライズドテスト
1 2 3 4 5 6 7 8 9 10 11 |
function sum(a: number, b: number): number { return a + b; } it.each` a | b | expected ${2} | ${3} | ${5} ${3} | ${-5} | ${-2} `("$aと$bを足すと$expectedになる", ({a,b,expected})=>{ expect(sum(a,b)).toBe(expected) }); |
同じ関数内で少し引数を変えてテストケースを書きたくなるケースは多いと思います。その場合はパラメタライズドテストという記法を使用するとまとめて記述することができます。
以下のように普通に個々に指定するのと同じ記述方法になります。
この記事へのコメントはありません。