0
0

More than 1 year has passed since last update.

[備忘録]jest.spyOn()でモック化する時に気をつけること

Last updated at Posted at 2022-09-12

はじめに

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()が動いてしまっている

image.png

なんとなくの推察

  1. sample.tsControllerコンストラクタで実行し保持しているModuleAのインスタンス
  2. 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)
})

image.png

その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.tsControllerクラス

  • 外部から取得できるように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
  }
}

image.png

所感

  • getterとはいえ、テストのためだけのメソッドが実装ファイルにあるのはなんか違和感
  • だからprototypeの方が良いかもーって思ってる
  • Controllerクラスを使う上位から、ModuleAのインスタンスをバケツリレーする方法もあるか
    • Reactとかでもそうだけど、バケツリレーはめんどいし依存性高まったりテスト容易性が低くなったりする気がしてるから避けてる
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0