Jestでモック関数を使って「イベントが呼び出された」ことを確認する


「イベントが呼び出されたかどうか」ということをモック関数を使って確かめる方法に関するメモ。

Jestそのものの導入、操作方法は[過去記事](https://qiita.com/NNNiNiNNN/items/1d6e01a8c33336364581)参照。

公式ドキュメントは以下。

https://jestjs.io/ja/docs/mock-functions

## テスト用のクラスを用意する


以下のようなクラスを用意した。
```ts
import { EventEmitter } from "events";
const emitter = new EventEmitter();

export class TestClass extends EventEmitter{
    constructor(){
        super();
    }

    testPing = () => {
        this.emit("testEvent", {"type": "test"});
    }
}
```

```testPing```関数を動かすと、```testEvent```イベントが帰ってくる。これが正常に動くかどうか確かめたい。

求めるのは以下のようなテストとなる。
* ```testEvent```が呼び出された際に引数内に```type: "test"```が含まれていれば**テストが通る**
* ```testEvent```が呼び出された際に引数内に```type: "test"```が含まれていなければ**テストが通らない**
* ```testEvent```が呼び出されなければ**テストが通らない**

## ダメな例

以下のようなテストコードを作ってみる。
```ts
import {TestClass} from "../src/eventTest";

it("should get event with correct type", () => {
    const testClass = new TestClass();

    testClass.on("testEvent", (msg) => {
        expect(msg).toEqual(
            expect.objectContaining({
              type: "test",
            })
          );        
    });
    testClass.testPing();
});
```

```TestClass```のインスタンスを作成し、```testPing```を動かす。

```testEvent```の発火時にイベントリスナーを動かし、受け取ったメッセージを確認する。ここに```type: "test"```というオブジェクトが含まれていれば通る。

これで一見よさそうだが、このまま動かすと以下のようなテストになる。

* ```testEvent```が呼び出された際に引数内に```type: "test"```が含まれていれば**テストが通る**
* ```testEvent```が呼び出された際に引数内に```type: "test"```が含まれていなければ**テストが通らない**
* ```testEvent```が呼び出されなければ**テストが通る**
    * 本来は**通ってはいけない**

例えばclassの方を少し書き換えてみる。

```ts
    testPing = () => {
        this.emit("testEvent_", {"type": "test"});
    }
```

```testEvent```ではなく```testEvent_```が発火するようにした。この場合先ほどのテストは通らないでほしいが...

```
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
```

**通ってしまう。** これはイベントの呼び出しの有無を確かめてるわけではないため。

これを正しくテストするために、モック関数というものを使用する。

## モック関数って何？

「関数がどのように呼び出されたか」ということを捉える関数。これを用いることで今まで何回その関数が呼び出されたか、どのような引数を持って呼び出されたかを取得することができる。

以下のようにして作成できる。

```ts
const testEventListener = jest.fn();
```

ここから、以下のようなexpect文で呼び出しに関する情報を調べることができる。

```ts
// 呼び出されたか否か
expect(testEventListener).toHaveBeenCalled();

// 指定の引数で呼び出されたことがあるか
expect(testEventListener).toHaveBeenCalledWith(arg1, arg2);

// 前回の呼び出しで指定の引数だったか
expect(testEventListener).toHaveBeenLastCalledWith(arg1, arg2);
```

## 正しい実装例

というわけで、以下のように実装できる。

```ts
import { TestClass } from "../src/eventTest";

it("should get event with correct type", () => {
    const testClass = new TestClass();
    const testEventListener = jest.fn();

    testClass.on("testEvent", (msg) => {
        testEventListener(msg);
    });

    testClass.testPing();

    expect(testEventListener).toHaveBeenCalledWith(
        expect.objectContaining({
            type: "test",
        })
    )
});
```
これで正常に動くようになる。

イベント発火時にはモック関数を動かすだけのリスナーを登録し、
```ts
    testClass.on("testEvent", (msg) => {
        testEventListener(msg);
    });
```
その後そのモック関数が呼ばれているかを確認した。

```ts
    expect(testEventListener).toHaveBeenCalledWith(
        expect.objectContaining({
            type: "test",
        })
    )
```
これで

* ```testEvent```が呼び出された際に引数内に```type: "test"```が含まれていれば**テストが通る**
* ```testEvent```が呼び出された際に引数内に```type: "test"```が含まれていなければ**テストが通らない**
* ```testEvent```が呼び出されなければ**テストが通らない**

という条件を満たすことができた。