👻

Jestのモックをより簡単に作れるライブラリを作った

2021/10/06に公開

Jestでクラスメソッドのモックをするとき、どう書いてますか?
こんなかんじでしょうか。

test.spec.ts
class A {
  method() {
    return "xxx";
  }
}

const aInstance = {
  method: jest.fn(),
} as A;

aInstance.method();
expect(aInstance.method).toBeCalledTimes(1);

ひとつだけならまあ問題ないように思います。
しかし、複数の関数を持つクラスの一つひとつに jest.fn() しなければならないとしたらどうでしょうか。
とっても面倒ですよね。

そこで、一撃で指定されたクラスをモックオブジェクトとして吐き出すライブラリを作成しました。

作ったもの

@kojiro.ueda/bandia です。スワヒリ語で偽物という意味だそうです。

このようにして使います。

class A {
  method() {
    return "xxx";
  }
}

const aInstance = mock<A>(); // ここ
aInstance.method.mockReturnValueOnce("yyy");
expect(aInstance.method()).toBe("yyy");
expect(aInstance.method).toBeCalledTimes(1);

こうです。どこにも jest.fn() が出てきませんが、jest.fnを使った時のように、mockReturnValueを設定できているのがおわかりになるでしょうか。

これで100メソッドあるクラスのモックが作りたくなっても簡単に作れるようになりましたね。めでたし。
・・・ではなく、このライブラリを使うことで得られるうれしみがあることを共有したかったのです。

うれしみ

1. 型定義でいい感じに書ける

下のコードを見てください。

class A {
  method(arg1: string, arg2: number): string {}
}

const a = mock<A>();
a.method.mockReturnValue(42); // 戻り値はstringでなければならないので、数字はエラーになる
// jest-whenというライブラリを使った場合こう書けますが・・・
when(a.method).calledWith("foo", "bar").mockReturnValue("hoge") // 第二引数はnumberなので文字列はエラーになる

上記を実現しようと思ったら、以下のように書く必要がありますね。

// const a = mock<A>();
const a = {
  method: jest.fn<string, [string, number]>()
}

これをいちいち書きたくないなあと思ったので作った次第であります。

2. テストコードが短くなる

モックを用意するのはなるべく短く書けた方がいいと思うので。
こんな感じで短くかけます。

しくみ

コードは20行くらいしかないので、読んでもらえればよいのですが、Proxyの機能を使ってメソッドへのアクセスがあったら jest.fn() の結果を返しているだけです。初回は jest.fn() したものを返し二回目以降は初回に作ったキャッシュしておいたモック関数を返しています。

あとは型定義を頑張っていて、 mock<A>() の戻り値はクラスAであり、クラスAの関数がモックされたものである、というふうに偽る型定義になっています。モックですから、型定義は偽っても大丈夫でしょう、ということです。

※注意点

勘のよい方はお気づきかもしれませんが、Proxyを使うやりかただと、ありとあらゆるメソッドをモックすることになります(型定義に存在しなくてもいい)。また、メソッドを、と言っていますが、やっていることはプロパティアクセスがあったときのモック関数を返す、ということなので、プロパティアクセスに対応できないのが問題点です。

class A {
  prop: string = "prop";
  method() {}
}

const a = mock<A>();
expect(typeof a.prop).toBe("string") // "function" になる

特に困っていないので対策は考えていませんが、Proxyをやめて、クラスを受け取ってprototypeを見るのかな。なんにせよ、不要だと思います。多分。

もしよければ使ってみてください。
以上です、よろしくお願いいたします。

Discussion