JavaScriptで単体テストする際は、Jestを使うのがデファクトになってきています。 単体テストでは、関連するモジュールをモックにしてテストすることが多いですね。 ここでは、Jestのモックの機能と使い方をユースケース別に説明します。

クラスのstaticなメソッドをモックしたい

例えば、自分の関数内でDate.now()を使って時間を取得していると、テストを実行するたびに値が異なるため、テストがうまくいかないことがあります。 そのような場合、jest.spyOnを使います。spyOnを使うことである特定の時間を返すことができるようになります。

class Test {
  func() {
    return Date.now();
  }
}

describe("Date#now", () => {
  it("spyOnを使うと好きな時間に固定することができる", () => {
    const spy = jest.spyOn(Date, "now");
    spy.mockReturnValue(1577804400000); // 2020/01/01
    expect(new Test().func()).toBe(1577804400000);
    spy.mockRestore();
  });
});

spy.mockRestore()を忘れてしまうと、他のすべてのテストでDate.now()がモックされた状態のままになってしまいます。 わざわざ書くのめんどいよ、という人は、JestのオプションにrestoreMocksという項目があるので、こちらの利用を検討してもよいでしょう。

インスタンスメソッドをモックしたい

インスタンスAの中でインスタンスBのメソッドを呼び出して、その結果を使って何かする、みたいな処理ってよくあるじゃないですか。

class ClassA {
  constructor(readonly b: ClassB) {}
  func() {
    return this.b.someFunc().toUpperCase();
  }
}

class ClassB {
  someFunc() {
    return "hello world.";
  }
}

ClassAのテストとしては、ClassBの関数を呼んだ結果をtoUpperCase()している、ということだけテストできればよいわけです。ClassBの実体を使っていると、ClassBの仕様が変わったときにClassAのテストも変えないといけなくなります。

そこで、ClassAのテストではClassBのモックを使います。 まずは、ただのオブジェクトにsomeFuncという関数を持たせる方法でやってみます。 ただのオブジェクトをClassBのインスタンスだと偽っており、立派なモックの役割を果たしています。

describe("ClassA#func", () => {
  it("ClassB#someFuncの結果をtoUpperCaseしていることをテストする", () => {
    const bMock = { someFunc: () => "foo" };
    const a = new ClassA(bMock);
    expect(a.func()).toBe("FOO");
  });
});

ClassBの関数が呼ばれたことも確認する

ところで、上記例では、a.func()の内部実装で"foo".toUpperCase()してても通っちゃうんですよね。ClassBのメソッドが呼ばれたかどうかは検証できていないわけです。呼ばれたことをあとで確認できるしくみはないでしょうか。

jest.fn()を使います。 これはモック関数と呼ばれる関数を返します。モック関数は呼ばれてもなにもしませんが、どのような引数で何回呼ばれたかを記録しています。また、必要であれば、戻り値を指定したり、内部実装を書いたりもできます。 これを使ってよりよくしてみましょう。

code.spec.ts

describe("ClassA#func", () => {
  it("ClassB#someFuncの結果をtoUpperCaseしていることをテストする", () => {
    const bMock = { someFunc: jest.fn() };
    bMock.someFunc.mockReturnValueOnce("foo");
    const a = new ClassA(bMock);
    expect(a.func()).toBe("FOO");
    expect(b.someFunc).toBeCalledTimes(1);
  });
});

いかがでしょうか。jest.fn()が返す関数オブジェクトは特殊で、いくつかのメソッドが生えており、ここではmockReturnValueOnceを使って、呼ばれたら一度だけ決まった値を返すように設定しています。 また、Jestが提供するexpectで関数が1度だけ呼ばれたことを確認しています。 これなら、ClassBのメソッドが呼ばれていそうなことも確認できましたね。

jest-whenでより読みやすい書き方にする

jest-whenというライブラリを追加してより読みやすい書き方にできます。

code.spec.ts