はじめに
jestを使ってユニットテストをしていると、関数やメソッドのモック化は頻繁に行う。
jest.spyOn()は、モック関数を作るのに便利なメソッドだが、つまづいたことがあったので備忘
事象について
私の今までの認識
-
jest.spyOn()
したメソッドは、下記を使って戻り値や実装を書き換えてやることで、実行されないと思っていた(このあたりのヤツ)mockResolvedValue
mockImplementation
- などなど
実際
- 気をつけないと実装したままのコードが実行されちゃう
- 特に
new
したインスタンスは、mockしようとしてるのが、実行されるインスタンスなのかをよく確認しないと、テスト実行時に、実装コード通りの動きをしてしまう
- 特に
コードサンプル
テストしたいファイル
- テスト対象
- クラス:
Controller
- メソッド:
testTarget
- クラス:
- テスト対象が依存するモジュール:
ModuleA
sample.ts
export class ModuleA {
public async exec(): Promise<string> {
// 外部APIなどテストで実行したくない処理
console.log('これが表示されちゃダメ')
return 'some results...'
}
}
export class Controller {
private _moduleA: ModuleA
public constructor() {
this._moduleA = new ModuleA()
}
/**
* testしたいメソッド
*/
public async testTarget(): Promise<boolean> {
// do something...
const r = await this._moduleA.exec()
return r.length ? true : false
}
}
実際に私がやっていたこと
テストコード
- まず依存している
ModuleA.exec()
の戻り値をmock - テスト対象メソッドを実行し、expect
sample.spec.ts
import { ModuleA, Controller } from './sample'
it('should return true', async () => {
// given ModuleA.exec()のモック化
const moduleA: ModuleA = new ModuleA()
jest.spyOn(moduleA, 'exec').mockResolvedValue('hoge')
// when Controller.testTarget()実行
const controller: Controller = new Controller()
// then resolveされたtrue
await expect(controller.testTarget()).resolves.toBe(true)
})
実行結果
- テスト自体はPASS
- しかし、実行されたくない
ModuleA.exec()
が動いてしまっている
なんとなくの推察
-
sample.ts
のController
コンストラクタで実行し保持しているModuleA
のインスタンス -
sample.spec.ts
の中で保持するModuleA
のインスタンス
もちろん両者は異なるオブジェクトであり、specファイルでmock化したのはあくまで「2」のインスタンスが保持するModuleA.exec()
だった
そのため、sample.ts
側ではモック化されていないModuleA.exec()
がコード通りに動いてしまった
違ってたらゴメン
対策
その1 prototype
を使う
jest.spyOn()
に渡す第1引数のオブジェクトとしてModuleA.prototype
を用いる
import { ModuleA, Controller } from './sample'
it('should return true', async () => {
// given ModuleA.exec()のモック化
- const moduleA: ModuleA = new ModuleA()
- jest.spyOn(moduleA, 'exec').mockResolvedValue('hoge')
+ jest.spyOn(ModuleA.prototype, 'exec').mockResolvedValue('hoge')
// when Controller.testTarget()実行
const controller: Controller = new Controller()
// then resolveされたtrue
await expect(controller.testTarget()).resolves.toBe(true)
})
その2 getter
でインスタンス化したオブジェクトをテストファイルで使用する
sample.spec.ts
- 実装ファイルのコンストラクタで生成されたインスタンスに対し
jest.spyOn()
する
import { ModuleA, Controller } from './sample'
it('should return true', async () => {
+ const controller: Controller = new Controller()
// given ModuleA.exec()のモック化
- const moduleA: ModuleA = new ModuleA()
+ const moduleA: ModuleA = controller.moduleA // getterからインスタンスを取得
jest.spyOn(moduleA, 'exec').mockResolvedValue('hoge')
// when Controller.testTarget()実行
- const controller: Controller = new Controller()
// then resolveされたtrue
await expect(controller.testTarget()).resolves.toBe(true)
})
sample.ts
のController
クラス
- 外部から取得できるように
getter
を作る
export class Controller {
private _moduleA: ModuleA
+ public get moduleA(): ModuleA {
+ return this._moduleA
+ }
public constructor() {
this._moduleA = new ModuleA()
}
/**
* testしたいメソッド
*/
public async testTarget(): Promise<boolean> {
// do something...
const r = await this._moduleA.exec()
return r.length ? true : false
}
}
所感
-
getter
とはいえ、テストのためだけのメソッドが実装ファイルにあるのはなんか違和感 - だから
prototype
の方が良いかもーって思ってる -
Controller
クラスを使う上位から、ModuleA
のインスタンスをバケツリレーする方法もあるか-
React
とかでもそうだけど、バケツリレーはめんどいし依存性高まったりテスト容易性が低くなったりする気がしてるから避けてる
-