💯Jestのモックを駆使してテストをよりよくする2020/12/03に公開2020/12/04JavaScriptTypeScriptJesttechJavaScriptで単体テストする際は、Jestを使うのがデファクトになってきています。単体テストでは、関連するモジュールをモックにしてテストすることが多いですね。ここでは、Jestのモックの機能と使い方をユースケース別に説明します。クラスのstaticなメソッドをモックしたい例えば、自分の関数内でDate.now()を使って時間を取得していると、テストを実行するたびに値が異なるため、テストがうまくいかないことがあります。そのような場合、jest.spyOnを使います。spyOnを使うことである特定の時間を返すことができるようになります。});});!spy.mockRestore()を忘れてしまうと、他のすべてのテストでDate.now()がモックされた状態のままになってしまいます。書くのめんどいよ、という人は、JestのオプションにrestoreMocksという項目があるので、こちらの利用を検討してもよいでしょう。インスタンスメソッドをモックしたいインスタンスAの中でインスタンスBのメソッドを呼び出して、その結果を使って何かする、みたいな処理ってよくあるじゃないですか。ClassAのテストとしては、ClassBの関数を呼んだ結果をtoUpperCase()している、ということだけテストできればよいわけです。ClassBの実体を使っていると、ClassBの仕様が変わったときにClassAのテストも変えないといけなくなります。そこで、ClassAのテストではClassBのモックを使います。ただのオブジェクトにsomeFuncという関数を持たせる方法でやってみます。ただのオブジェクトをClassBのインスタンスだと偽っており、立派なモックの役割を果たしています。});});ClassBの関数が呼ばれたことも確認するところで、上記例では、a.func()の内部実装で"foo".toUpperCase()してても通っちゃうんですよね。ClassBのメソッドが呼ばれたかどうかは検証できていないわけです。呼ばれたことをあとで確認できるしくみはないでしょうか。jest.fn()を使います。これはモック関数と呼ばれる関数を返します。モック関数は呼ばれてもなにもしませんが、どのような引数で何回呼ばれたかを記録しています。必要であれば、戻り値を指定したり、内部実装を書いたりもできます。これを使ってよりよくしてみましょう。});});いかがでしょうか。jest.fn()が返す関数オブジェクトは特殊で、いくつかのメソッドが生えており、ここではmockReturnValueOnceを使って、呼ばれたら一度だけ決まった値を返すように設定しています。Jestが提供するexpectで関数が1度だけ呼ばれたことを確認しています。これなら、ClassBのメソッドが呼ばれていそうなことも確認できましたね。jest-whenでより読みやすい書き方にするjest-whenというライブラリを追加してより読みやすい書き方にできます。});});expectCalledWithを使うことで、それ以外の引数で関数が呼ばれたらエラーになるようになっています。これで、引数なしで1度だけ呼ばれたことまで確認できるようになりました。外部ライブラリをまるっとモック化したいテスト時に外部ライブラリの挙動を制御したいことってありますよね。ランダムな数値を得るためにrandomというライブラリを入れている場合など、テストが運ゲーになってしまいます。});このような問題を避けるため、外部のライブラリをモックする方法があります。外部のライブラリをモックするjest.mock("random")を使います。これを書いておくと、このテストファイルを実行している間、randomライブラリはjest.fn()でモック関数化された状態になります。floatという関数もモック関数になっていますから、インポートした関数にmockReturnValueOnceメソッドが生えています。これで、祈らなくても通ってくれるようになります。});});問題は、上記テストファイル内ではfloatは実体を使うことができなくなることです。(モックされた状態から戻せない)外部のライブラリをモックしたりモックしなかったりするrandomFloatのテストを例に説明します。以下に3件のテストケースを挙げています。説明が必要そうなところに、直接コメントで記載しています。requireやimportで使われるキャッシュを破棄する。前のテストでdoMockしているので、それを打ち消している。前のテストでdontMockしているので、それをさらに打ち消している。jest.resetModules()そもそも、テスト実行しているNode.jsは、require("moduleName")を初めて実行したときにそのモジュールファイルを読み込みにいきます。このとき、モジュールはキャッシュされ、次回以降のrequireではそのキャッシュが読み込まれることになります。doMockするよりも前に、requireした場合、モックされていないモジュールがキャッシュされているため、doMockしても意味がないのです。そこで、以下のような流れでモジュールを読み込むことで、モジュールを明示的に好きなタイミングでモックすることができるようになります。doMockしたあとmyRandomも読み込みなおすrandomモジュールをdoMockしたあと、myRandomを読み込みなおしています。理由は、doMockより前にmyRandomを読み込んだ場合、myRandomは内部でrandomモジュールをインポートしているため、doMockが効かない(既に実体が読み込まれてしまっている)からです。キャッシュを削除し、doMockでモックすることを宣言し、関連しているモジュールをimportし直さなければ狙った通りに動いてくれません。dontMockは必要か?必要です。doMockでモックすることを宣言しているため、宣言したあとは何度キャッシュを消してもモックされたモジュールが読み込まれます。そこで、requireの前にdontMockでモックしないことを宣言する必要があるのです。import文も使えるrequireの代わりに、動的importも使えます。やることはあまり変わりませんが、requireかimport、どっちかに統一して書いた方がよいでしょう。まとめ以上、いくつかのユースケースを例にあげてJestのモックの使い方を説明しました。私はjest.fn()くらいしか使っていませんが、他の例もトリッキーなパターンでは必要になることもあるでしょう。そもそも、テストがしづらい場合には何か設計がおかしいことも多いので、それを見直してみるのもよいでしょう。