【備忘録】JestのspyOn()とmock()の使い方について
はじめに
jestは、javascriptやtypescriptのテストツールです。
jest.spyOn()
とjest.mock()
は、どちらもメソッドをmockするためのもので、テストコードの中でモック関数を定義する際に使用します。
どちらも同じようなことが出来るのですが、いつもいざ使おうとしたときに混同してしまいがちなので、備忘録としてまとめてみました。
環境
テストを作成した環境は、以下の通りです。
- node: 12.19.0
- @types/jest: 26.0.19
- jest: 26.6.3
- ts-jest: 26.4.4
- ts-node: 9.0.0
- typescript: 4.1.2
基本的な使い方
jest.spyOn()
は、オブジェクトを引数に指定するのに対し、jest.mock()
は、モジュールを引数に指定します。
つまり、mockの対象が引数に指定したオブジェクトだけなのか、モジュールそのものなのかという違いがあります。
また、jest.mock()
でmockされモジュールは、デフォルトでは何も返さない状態になってしまうのに対し、jest.spyOn()
の場合、デフォルトで本来のメソッドが呼ばれます。(JEST API Reference#jest.spyOnより)
テスト対象サンプルコード
以下のサンプルコードで、定義されたTestClass
クラスのgetText()
メソッドをテストする場合を例に記載します。
- moduleA.ts:mock対象。
CalcModuleA
クラスとTextModuleA
クラスが定義されたファイル - test.ts:テスト対象。
TesTClass
クラスが定義されたファイル
moduleA.ts
export class CalcModuleA {
constructor(readonly x: number){}
exec(a: number, b: number) {
return a + b - this.x;
}
}
export class TextModuleA {
exec(a: string, b: string) {
return a + b;
}
}
test.ts
import { CalcModuleA, TextModuleA } from './moduleA';
export class TestClass {
calc: CalcModuleA;
constructor(readonly text: TextModuleA) {
this.calc = new CalcModuleA(1);
}
public getText(textA: string, textB: string) {
const s1 = textA.split('s');
const s2 = textB.substr(-2);
const c = this.calc.exec(textA.length, textB.length);
const t = this.text.exec(s1[0], s2);
return t + s1[s1.length -1] + c;
}
}
jest.spyOn()
でmockする場合
TextModuleA.exec mock
テスト内で、TextModuleA
クラスのインスタンスを作成後、jest.spyOn()
でmock化し処理を定義します。
import { CalcModuleA, TextModuleA } from '../moduleA';
import { TestClass } from '../test';
describe('spyOn', () => {
it(`TextModuleA.exec mock`, () => {
//TestClassクラスに渡すTextModuleAクラスのインスタンスを作成
const t = new TextModuleA();
//execメソッドのmock化
const mockTextExec = jest.spyOn(t, 'exec');
//mock処理を仕込む
mockTextExec.mockImplementation((a: string, b: string) => {
return `spy`;
});
const test = new TestClass(t);
//testインスタンス内のCalcModuleAクラスのexecメソッドをmock化
//インスタンスが取得さえ出来れば、mock化することができる。
const mockCalcExec = jest.spyOn(test['calc'], 'exec');
//テスト対象のメソッドを実行
const result = test.getText('testA', 'planB');
console.log(`spyOn result: ${result}`);
//mock化したメソッドの引数を検証
expect(mockTextExec).toBeCalledTimes(1);
expect(mockTextExec).toBeCalledWith('te', 'nB');
expect(mockCalcExec).toBeCalledTimes(1);
expect(mockCalcExec).toBeCalledWith(5, 5);
//テスト対象メソッドの結果を検証
expect(result).toBe('spytA9');
})
});
jest.mock()
でmockする場合
テストファイルの最初で、mock化を行います。
TestModuleA
クラスをmock化する際に、CalcModuleA
クラスも一緒にmock化されてしまうため、一緒に処理を定義しておく必要があります。
jest.mock('../moduleA', () => {
//moduleAモジュールのimport結果をmock
return {
//TextModuleAクラスのコンストラクタのmock定義
TextModuleA: jest.fn().mockImplementation(() => {
return {
exec: jest.fn(),
}
}),
//CalcModuleAクラスのコンストラクタのmock定義
CalcModuleA: jest.fn().mockImplementation(() => {
return {
//execメソッドをmock化し、常に1を返すように設定
exec: jest.fn().mockReturnValue(1),
}
})
}
});
import { TextModuleA } from '../moduleA';
import { TestClass } from '../test';
describe('jest.mock', () => {
it(`TextModuleA.exec mock`, () => {
//TestClassクラスに渡すTextModuleAクラスのインスタンスを作成
const t = new TextModuleA();
//execメソッドのmock定義を取得
const mockTextExec = t.exec as jest.Mock;
//mock処理を仕込む
mockTextExec.mockImplementation((a: string, b: string) => {
return `mock`;
});
const test = new TestClass(t);
//testインスタンス内のCalcModuleAクラスのexecメソッドのmock定義を取得
const mockCalcExec = test['calc'].exec;
//テスト対象のメソッドを実行
const result = test.getText('testA', 'planB');
console.log(`mock result: ${result}`);
//mock化したメソッドの引数を検証
expect(mockTextExec).toBeCalledWith('te', 'nB');
expect(mockCalcExec).toBeCalledWith(5, 5);
//テスト対象メソッドの結果を検証
expect(result).toBe('mocktA1');
});
})
この例では、特定のインスタンスの1メソッドをmockして1件だけのテストをしているのみであるため、jest.spyOn()
の方がjest.mock()
に比べ、シンプルに記述することができます。
mockの結果を呼び出し毎に設定する場合
複数回呼ばれるメソッドで、都度異なる結果を返すようにしたい場合のmock処理の定義方法です。
xxxOnce()
でmock定義することで1度だけ呼ばれるようになります。複数個のxxxOnce()
を連続で定義すると、定義した順番にその値が返されるようになります。
xxxOnce()
が全て返された後は、xxxOnce()
でないmock処理が定義されていた場合は、その値を返しますが、何も定義されていない場合、デフォルトの処理が呼ばれます。
it('mock returnValues', () => {
//TestClassクラスに渡すTextModuleAクラスのインスタンスを作成
const t = new TextModuleA();
//execメソッドのmock化
const mockTextExec = jest.spyOn(t, 'exec');
//mock処理を仕込む
mockTextExec.mockImplementationOnce((a: string, b: string) => {
return `spy1`;
}).mockImplementationOnce(() => 'spy2')
.mockReturnValueOnce('spy3');
//先に定義した値から順番に返される
expect(t.exec('a', 'b')).toBe('spy1');
expect(t.exec('a', 'b')).toBe('spy2');
expect(t.exec('a', 'b')).toBe('spy3');
//定義したものを全て呼んだ後は、デフォルトの処理が呼ばれる
expect(t.exec('a', 'b')).toBe('ab');
})
mockの初期化処理
mockした処理が呼ばれた後、そのまま別のテストを行うと、定義済みのmockが影響して上手くテストが出来ないことがあり得ます。そこで、テストの前後どちらかでmockの初期化処理を行うようにすべきです。
初期化に関するメソッドは、以下の3つがあります。(JEST API Referenceより)
-
mockClear()
:mockの実行履歴(呼び出し回数など)をクリアします。 -
mockReset()
:mockClear()
の内容に加えて、mockの定義(戻り値や、関数)も完全に初期化する。jest.spyOn()
のものに使用した場合でも、戻り値のない関数となるので注意が必要です。 -
mockRestore()
:jest.spyOn()
のものにのみ有効です。mockReset()
の内容に加えて、対象をmock前の処理に復元します。
最低でもmockClear()
は行うようにした方が良いです。
少しひねった使い方
jest.spyOn()
で、インスタンスを取得せずにmockする方法
jest.spyOn()
で、インスタンスを取得せずにmockする方法基本的に、jest.mock()
でmockすれば良いですが、jest.spyOn()
でも出来ます。
mock対象のメソッドが一部であったり、mock処理を実装せず、引数の検証だけしたい場合などは、jest.spyOn()
を使用した方が容易となります。
jest.spyOn()
で、引数に指定するオブジェクトとしてxxx.prototype
(xxx
は、クラス名)を使用することでmockすることができます。
ただし、mock対象がインスタンスではないため、テストの前後のどちらかで、mockRestore()
などをしないと、ほかのテストに影響が出てしまうので、注意が必要です。
import { CalcModuleA, TextModuleA } from '../moduleA';
import { TestClass } from '../test';
describe('spyOn prototype', () => {
beforeEach(() => {
//CalcModuleAクラスのexecメソッドをmock化
jest.spyOn(CalcModuleA.prototype, 'exec');
//TextModuleAクラスのexecメソッドのmock化
jest.spyOn(TextModuleA.prototype, 'exec');
});
afterEach(() => {
//test外で、mock定義を行うため、`afterEach()`で、mockの初期化やクリア処理を行う。
//CalcModuleAクラスのexecメソッドのmock履歴をクリア
(CalcModuleA.prototype.exec as jest.Mock).mockClear();
//TextModuleAクラスのexecメソッドのmock定義を本来のものに戻す
(TextModuleA.prototype.exec as jest.Mock).mockRestore();
})
it(`TextModuleA.exec mock`, () => {
//TextModuleAクラスのexecメソッドのmock処理を仕込む
(TextModuleA.prototype.exec as jest.Mock).mockImplementation((a: string, b: string) => {
return `spy`;
});
//TestClassクラスに渡すTextModuleAクラスのインスタンスを作成
const t = new TextModuleA();
const test = new TestClass(t);
//テスト対象のメソッドを実行
const result = test.getText('testA', 'planB');
console.log(`spyOn result: ${result}`);
//mock化したメソッドの引数を検証
expect(TextModuleA.prototype.exec).toBeCalledTimes(1);
expect(TextModuleA.prototype.exec).toBeCalledWith('te', 'nB');
expect(CalcModuleA.prototype.exec).toBeCalledTimes(1);
expect(CalcModuleA.prototype.exec).toBeCalledWith(5, 5);
//テスト対象メソッドの結果を検証
expect(result).toBe('spytA9');
});
});
jest.spyOn()
でコンストラクタをmockする方法
こちらも、jest.mock()
でmockすれば良いですが、jest.spyOn()
でも出来ます。
jest.spyOn()
で、引数に指定するオブジェクトとしてモジュール自体を指定することで、コンストラクタのmockをすることができます。
ただし、他のjest.spyOn()
と異なりデフォルトでも本来のメソッドが呼び出されません。
そのため、テストの際にmock処理を定義する必要があります。
CalcModuleAクラスのコンストラクタをmockする例
moduleAのCalcModuleAクラスのコンストラクタをmockするサンプルです。
本来のコンストラクタが呼び出せるように、jest.spyOn()
を行う前にoriginal
変数に格納し、spyOn()
時の初期mock処理として呼び出すようにmock処理を定義しています。
//moduleAモジュール全体を読み込む
import * as moduleA from '../moduleA';
import { TextModuleA, CalcModuleA } from '../moduleA';
import { TestClass } from '../test';
describe('spyOn constructor', () => {
//mock前の本来の処理を格納する。
const original = moduleA.CalcModuleA;
beforeEach(() => {
//mockしたいコンストラクタのクラスを指定することで、コンストラクタをmockできる。
//デフォルトの処理として、本来のコンストラクタを呼び出すようにする。
jest.spyOn(moduleA, 'CalcModuleA').mockImplementation((x)=> {return new original(x)});
})
afterEach(() => {
//CalcModuleAクラスのコンストラクタのmock定義を本来のものに戻す
(CalcModuleA as jest.Mock).mockRestore();
})
it(`CalcModuleA.exec mock`, () => {
//CalcModuleAクラスのコンストラクタをmockし、execメソッドが常に1000を返すように設定
const mockCalcExec = jest.fn((a, b) => { return 1000 });
(CalcModuleA as jest.Mock).mockImplementation((x: number) => {
return {
x,
exec: mockCalcExec
}
});
const t = new TextModuleA();
const test = new TestClass(t);
const result = test.getText('testA', 'planB');
console.log(`spyOn result: ${result}`);
//mock化したコンストラクタの呼び出し回数と引数を検証
expect(CalcModuleA).toBeCalledTimes(1);
expect(CalcModuleA).toBeCalledWith(1);
//mock化したexecメソッドの引数を検証
expect(mockCalcExec).toBeCalledWith(5, 5);
expect(result).toBe('tenBtA1000');
});
})
Dateをmockする例
globalのDateクラスのコンストラクタをmockするサンプルです。
new Date()
の結果を、固定するためのmockです。コンストラクタの引数が無い場合のみmockした値を返すようにmock定義を行っています。
jest.spyOn()
で定義しているため、mockRestore()
するだけで、本来の処理に復元が可能です。
//mockする前の本来の処理を格納
const OriginalDate = Date;
//mockで返す固定のDateを定義
const now = new OriginalDate('2020/04/01 11:23:34.567');
const spiedDate = jest.spyOn<any, any>(global, 'Date').mockImplementation((...arg: any[]) => {
const p = Array(7).fill(0);
arg.forEach((v, i) => p[i] = v);
if (arg.length > 0) {
return arg.length === 1 ? new OriginalDate(arg[0]) : new OriginalDate(p[0], p[1], p[2], p[3], p[4], p[5], p[6]);
}
return now;
});
・・・
//mockを解除して本来の処理に戻す際に呼ぶ
spiedDate.mockRestore();
jest.mock()
で、本来のメソッドを残しつつmockする方法
jest.mock()
とjest.requireActual()
を使用することで、jest.spyOn()
と同様に、コンストラクタやメソッドをspyしつつ本来の処理を呼び出すことができます。(JEST API Reference#jest.requireActualより)
CalcModuleAクラスのコンストラクタをmockする例
import { CalcModuleA, TextModuleA } from '../moduleA';
import { TestClass } from '../test';
//moduleAモジュールの本来のモジュール
let original: any;
jest.mock('../moduleA', () => {
//moduleAモジュールの本来のモジュールを読み込む
original = jest.requireActual('../moduleA');
return {
//moduleAモジュールの本来の処理を展開
...original,
//CalcModuleAクラスのコンストラクタ定義を上書き。
CalcModuleA: jest.fn().mockImplementation((x: number) => {
//初期処理として本来のコンストラクタを呼び出し
return new original.CalcModuleA(x);
}),
}
});
describe('jest.mock constructor', () => {
afterEach(() => {
//CalcModuleAクラスのコンストラクタのmock履歴をクリア
(CalcModuleA as jest.Mock).mockClear();
//コンストラクタのmock定義がテストごとに変わる場合は、再定義する。
(CalcModuleA as jest.Mock).mockImplementation((x: number) => {
//初期処理として本来のコンストラクタを呼び出し
return new original.CalcModuleA(x);
});
})
it(`arguments check`, () => {
//TestClassクラスに渡すTextModuleAクラスのインスタンスを作成
const t = new TextModuleA();
const test = new TestClass(t);
//テスト対象のメソッドを実行
const result = test.getText('testA', 'planB');
console.log(`mock result: ${result}`);
//mock化したコンストラクタの引数を検証
expect(CalcModuleA).toBeCalledTimes(1);
expect(CalcModuleA).toBeCalledWith(1);
//テスト対象メソッドの結果を検証
expect(result).toBe('tenBtA9');
});
it(`CalcModuleA.exec mock`, () => {
//CalcModuleAクラスのコンストラクタをmockし、execメソッドが常に1000を返すように設定
const mockCalcExec = jest.fn((a, b) => { return 1000 });
(CalcModuleA as jest.Mock).mockImplementation((x: number) => {
return {
exec: mockCalcExec
}
});
const t = new TextModuleA();
const test = new TestClass(t);
const result = test.getText('testA', 'planB');
console.log(`mock2 result: ${result}`)
expect(CalcModuleA).toBeCalledTimes(1);
expect(CalcModuleA).toBeCalledWith(1);
//mock化したexecメソッドの引数を検証
expect(mockCalcExec).toBeCalledWith(5, 5);
expect(result).toBe('tenBtA1000');
})
})
pathモジュールのisAbsolute()
をmockする例
自作のモジュールでなくても、同様にmockすることが可能です。
import path from 'path';
jest.mock('path', () => {
//pathモジュールの本来の処理を格納
const original = jest.requireActual('path');
return {
...original,
//isAbsolute()の処理をmockに差し替え
isAbsolute: jest.fn((filePath: string) => {
return original.isAbsolute(filePath);
}),
}
})
describe('path', () => {
it('isAbsolute', () => {
//1回目は、必ずtrueを返すようにmock定義を追加
(path.isAbsolute as jest.Mock).mockReturnValueOnce(true);
//1回目は、trueが返される
expect(path.isAbsolute('./test.ts')).toBe(true);
//joinメソッドは、本来の処理が呼ばれる
expect(path.join('z', 'a')).toBe('z\\a');
//2回目は、本来の処理が呼ばれ、falseが返される
expect(path.isAbsolute('./test.ts')).toBe(false);
});
});
fsモジュールのexistsSync()
をmockする例
本来の処理を、originalExistsSyncFunction
としてmock処理の外部に格納することで、テストの際に必要に応じて本来の処理を呼ぶことが出来ます。(例:テストの結果としてファイルの存在確認をしたい。など)
//existsSync()の本来の処理を格納する変数
//あとから、本来の処理を呼べるようにするために定義しておく。
let originalExistsSyncFunction;
jest.mock('fs', () => {
const original = jest.requireActual('fs');
//existsSync()の本来の処理を格納
originalExistsSyncFunction = original.existsSync;
return {
...original,
existsSync: jest.fn((filePath: string) => {
//デフォルトで、本来の処理を行うようにmockを定義
return originalExistsSyncFunction(filePath);
}),
};
});
describe('fs', () => {
it('existsSync', () => {
//常にfalseを返すように定義
(fs.existsSync as jest.Mock).mockReturnValue(false);
expect(originalExistsSyncFunction('./write.txt')).toBe(false);
expect(fs.existsSync('./write.txt')).toBe(false);
fs.writeFileSync('./write.txt', 'write');
expect(originalExistsSyncFunction('./write.txt')).toBe(true);
expect(fs.existsSync('./write.txt')).toBe(false);
expect(fs.existsSync).toBeCalledTimes(2);
})
});
おわりに
今回、jest.spyOn()
とjest.mock()
を使ったmockの方法についてまとめてみました。
両方とも出来ることはほぼ同じですが、mockを行う際の指定方法やmockの初期化時の挙動に違いがある点は、気を付ける必要があります。
個人的には、jest.spyOn()
の方が手軽に使えるのかなという印象です。
記載の会社名、製品名、サービス名等はそれぞれの会社の商標または登録商標です。
Author And Source
この問題について(【備忘録】JestのspyOn()とmock()の使い方について), 我々は、より多くの情報をここで見つけました https://qiita.com/m-yo-biz/items/e9b6298d111ff6d03a5e著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .