[TypeScript] Jestでインスタンスのモックを作る方法


テスト対象

gateway.ts
export interface Driver {
  fetch: () => string;
}

export class Gateway {
  constructor(private driver: Driver) {}

  fetch() {
    const text = this.driver.fetch();
    return text;
  }
}

GatewayクラスはコンストラクタにDriverインターフェイスが実装されたインスタンスを受け取ります。
fetch()メソッドで受け取ったインスタンスのfetch()メソッドを更に呼び出します。

実際にGatewayクラスを使う場合は下のようなコードになります。

// Driverインターフェイスを実装したTestDriverクラスを定義
class TestDriver implements Driver {
  fetch() {
    return 'test'
  }
}
const driver = new TestDriver();
// TestDriverクラスのインスタンスをコンストラクタで受け取る
const gateway = new Gateway(driver);
gateway.fetch();

今回はGatewayクラスのfetch()メソッドのテストを書いていきます。

テストコード

fetch()メソッドのテストコードを書いていくと下のようになります。

gateway.spec.ts
import { Driver, Gateway } from './gateway.ts'

it('gateway fetch success', () => {
  const expected = 'test';

  const driver = ????
  const gateway = new Gateway(driver);
  const actual = gateway.fetch();
  expect(actual).toBe(expected)
})

Gatewayのインスタンスを作るときに困ったことが起きます。
GatewayクラスのコンストラクタはDriverインターフェイスのインスタンスを受け取る必要があります。

しかし、gateway.tsにはDriverインターフェイスを実装したクラスは存在しません。
なのでインスタンスを用意することができません。

ここでモックを使うことでインスタンスを擬似的に用意することができます。

インスタンスのモックを実装

モックを実装するにはjest.fn()を使います。

const driverMock = jest.fn<Driver, []>();

ここでfnのgenericsの第一引数にモックするインスタンスの型(インターフェイスなども含む)、第二引数にコンストラクタのパラメータの型を設定します。
今回モックしたいのはDriverのインスタンスなので、第一引数にDriver、第二引数に[](空配列)を設定しています。

これに加えてGatewayクラスが呼び出すDriverのfetch()メソッドの実装をモックに定義する必要があります。
モック
実装を定義するにはmockImplementation()メソッドを使います。

const DriverMock = jest.fn<TestDriver, []>().mockImplementation(() => {
  return {
    fetch: () => {
      return 'test'
    }
  }
})
const driver = new DriverMock();

mockImplementation()メソッドに関数を引数に渡します。
引数に渡す関数はモックに定義したいメソッドを含んだオブジェクトを返すように定義します。
今回はfetch()メソッドを実行すると'test'を返すように定義しています。

最終的にgateway.spec.tsは下のようになります。

gateway.spec.ts
import { Driver, Gateway } from './gateway.ts'

it('gateway fetch success', () => {
  const expected = 'test';

  const DriverMock = jest.fn<TestDriver, []>().mockImplementation(() => {
    return {
      fetch: () => {
        return 'test'
      }
    }
  })
  const driver = new DriverMock();
  const gateway = new Gateway(driver);
  const actual = gateway.fetch();
  expect(actual).toBe(expected)
})

メソッドが呼ばれたかテストする

上のテストコードでは作成したモックのfetch()メソッドが本当に呼ばれているかがテストされていません。
もしかしたらdriverのfetch()メソッドを呼ばずにテストが通ってしまっている可能性もあります。
正しくfetch()が呼ばれるかをテストするテストコードを加えてみましょう。

関数が呼ばれたかどうかをテストするにはexpectのtoBeCalled()メソッドを使います。
expectに関数を渡してtoBeCalled()メソッドを呼ぶことで、expectに渡された関数が実行されたかどうかをテストできます。

expect(function).toBeCalled();

fetch()メソッドが呼ばれたかどうかをテストするテストコードを加えると下のようになります。

gateway.spec.ts
import { Driver, Gateway } from './gateway.ts'

it('gateway fetch success', () => {
  const expected = 'test';

  const fetchFunctionMock = jest.fn(() => {
    return 'test'
  })
  const DriverMock = jest.fn<TestDriver, []>().mockImplementation(() => {
    return {
      fetch: fetchFunctionMock
    }
  })
  const driver = new DriverMock();
  const gateway = new Gateway(driver);
  const actual = gateway.fetch();
  expect(actual).toBe(expected)
  expect(fechFunctionMock).toBeCalled();
})

fetch()メソッドは変数のfetchFunctionに一旦入れています。
変数に入れることでexpect()の引数に渡せるようにしています。