[Jest] Mocking


現在再設計中のNode/Serpressベースのプロジェクトでは、Jest/Sertestをテストライブラリとして使用しています.突然Jestが提供するMock機能に興味を持ち、書くことにした.多くのブログや公式文書を参考にして、最も理解しやすい文章をリンクに残しました.
https://www.daleseo.com/jest-fn-spy-on/
https://medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c
Mocking
Mockingは,セルテストを記述する際に,コード依存部分の代わりに偽(mock)を用いる方法である.通常、この方法は、テストするコードに依存する部分を直接生成できない場合に使用します.
たとえば、データベースからデータを削除するコードのユニットテストを記述する場合、実際のデータベースを使用すると、多くの問題が発生する可能性があります.
  • データベース接続と同様に、I/O操作を含むテストでは、テストの実行速度が大幅に低下することがあります.
  • プロジェクトの規模が拡大するにつれて、一度に実行する必要があるテストケースが増え、これらの小さな速度の低下が大きな話題になる可能性があります.CI/CDパイプラインの一部としてテストを自動化し、頻繁に実行する必要がある場合は、より大きな問題になる可能性があります.
  • テスト自体のコードよりも、データベースとの接続、トランザクションの作成、クエリーの送信のコードの方が長い場合があります.つまり、お腹よりおへそが大きいかもしれません.
  • テスト実行時に、データベースが一時的に接続できない場合、テストは失敗します.したがって、テストはインフラストラクチャ環境の影響を受けます.
  • のテストが終了すると、データベースはテストによって変更されたデータをロールバックする必要があります.これはかなり面倒な作業かもしれません.
  • 最も重要なのは、このようにテストを記述すると、「ユニットテスト」(Unit Test)の基本思想に合致しない、すなわち特定の機能のみをテストすることである.すなわち,単位テストは依存する部分とは独立したテストであり,依存する部分の影響を受けてはならない.
    この場合、Mockingは、偽りのオブジェクトを生成するメカニズムを提供する.また、テスト実行中に偽オブジェクトに何が起こったかを覚えているため、偽オブジェクトが内部でどのように使用されているかを検証できます.要するに、mockingを使用すると、依存する部分に依存することなく、実際のオブジェクトを使用するよりも、より簡単で迅速なユニットテストを実現できます.
    以下、公式文書の参照を開始します.
    https://jestjs.io/docs/mock-functions
    Using a mock function
    foreach関数の実装をテストすると、配列内の各項目のコールバックが呼び出されます.
    function forEach(items, callback) {
      for (let index = 0; index < items.length; index++) {
        callback(items[index]);
      }
    }
    mock関数を使用してこの関数をテストし、mockオブジェクトのステータスを確認して、コールバックが予想通りに呼び出されることを確認できます.
    const mockCallback = jest.fn(x => 42 + x);
    forEach([0, 1], mockCallback);
    
    // The mock function is called twice
    expect(mockCallback.mock.calls.length).toBe(2);
    
    // The first argument of the first call to the function was 0
    expect(mockCallback.mock.calls[0][0]).toBe(0);
    
    // The first argument of the second call to the function was 1
    expect(mockCallback.mock.calls[1][0]).toBe(1);
    
    // The return value of the first call to the function was 42
    expect(mockCallback.mock.results[0].value).toBe(42);
    .mock property
    各mock関数には特殊なmock属性があります.このプロパティには、関数呼び出し方法と関数を返すデータが含まれます.mockプロパティは、呼び出すたびに値を追跡するため、チェックすることもできます.
    const myMock = jest.fn();
    
    const a = new myMock();
    const b = {};
    const bound = myMock.bind(b);
    bound();
    
    console.log(myMock.mock.instances);
    // > [ <a>, <b> ]
    これらのmockプロパティの各プロパティは、テストがこれらの関数を呼び出し、インスタンス化し、返す方法を決定するのに役立ちます.
    // The function was called exactly once
    expect(someMockFunction.mock.calls.length).toBe(1);
    
    // The first arg of the first call to the function was 'first arg'
    expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
    
    // The second arg of the first call to the function was 'second arg'
    expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
    
    // The return value of the first call to the function was 'return value'
    expect(someMockFunction.mock.results[0].value).toBe('return value');
    
    // This function was instantiated exactly twice
    expect(someMockFunction.mock.instances.length).toBe(2);
    
    // The object returned by the first instantiation of this function
    // had a `name` property whose value was set to 'test'
    expect(someMockFunction.mock.instances[0].name).toEqual('test');
    Mock Return Values
    mock関数は、テスト中にコードにテスト値を注入するためにも使用できます.
    const myMock = jest.fn();
    console.log(myMock());
    // > undefined
    
    myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
    
    console.log(myMock(), myMock(), myMock(), myMock());
    // > 10, 'x', true, true
    mock関数は,機能的連続伝達スタイルを用いたコードにおいても非常に有効である.このスタイルを使用して作成されたコードは、複雑なルートの必要性を回避するのに役立ちます.このルートは、テストで値が直面する実際のコンポーネントの動作を直接注入するために使用できます.
    const filterTestFn = jest.fn();
    
    // Make the mock return `true` for the first call,
    // and `false` for the second call
    filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
    
    const result = [11, 12].filter(num => filterTestFn(num));
    
    console.log(result);
    // > [11]
    console.log(filterTestFn.mock.calls[0][0]); // 11
    console.log(filterTestFn.mock.calls[1][0]); // 12
    Mocking Modules
    APIからユーザをインポートするクラスがあると仮定します.クラスはaxiosを使用してAPIを呼び出し、すべてのユーザーを含むデータ属性を返します.
    import axios from 'axios';
    
    class Users {
      static all() {
        return axios.get('/users.json').then(resp => resp.data);
      }
    }
    
    export default Users;
    APIではなくjestを使用してこのメソッドをテストします.mock()関数を使用してaxiosモジュールを自動的にシミュレートできます.
    モジュールをシミュレーションすると、テストは宣言するデータを返します.getにはmockResolveValueを提供できます.axios.get(「/users.json」)は偽の応答を提供します.
    import axios from 'axios';
    import Users from './users';
    
    jest.mock('axios');
    
    test('should fetch users', () => {
      const users = [{name: 'Bob'}];
      const resp = {data: users};
      axios.get.mockResolvedValue(resp);
    
      // or you could use the following depending on your use case:
      // axios.get.mockImplementation(() => Promise.resolve(resp))
    
      return Users.all().then(data => expect(data).toEqual(users));
    });
    Mocking Partials
    mockingは部分的に実行してもよい.すなわち、モジュールの特定のサブセットをシミュレートすることができ、残りのモジュールは実際の実装を維持することができる.
    // foo-bar-baz.js
    export const foo = 'foo';
    export const bar = () => 'bar';
    export default () => 'baz';
    //test.js
    import defaultExport, {bar, foo} from '../foo-bar-baz';
    
    jest.mock('../foo-bar-baz', () => {
      const originalModule = jest.requireActual('../foo-bar-baz');
    
      //Mock the default export and named export 'foo'
      return {
        __esModule: true,
        ...originalModule,
        default: jest.fn(() => 'mocked baz'),
        foo: 'mocked foo',
      };
    });
    
    test('should do a partial mock', () => {
      const defaultExportResult = defaultExport();
      expect(defaultExportResult).toBe('mocked baz');
      expect(defaultExport).toHaveBeenCalled();
    
      expect(foo).toBe('mocked foo');
      expect(bar()).toBe('bar');
    });
    Mock Implementations
    また,シミュレーション戻り値に加えて,シミュレーション関数に完全に代わる機能もある.これはjestですfnまたはmock関数のmock Implementation onceメソッドを使用して実行できます.
    jest.mock('../foo'); // this happens automatically with automocking
    const foo = require('../foo');
    
    // foo is a mock function
    foo.mockImplementation(() => 42);
    foo();
    // > 42
    複数の関数呼び出しで異なる結果を生成するために、mock関数に複雑なアクションを作成する必要がある場合は、mockImplementOnceメソッドを使用します.
    const myMockFn = jest
      .fn()
      .mockImplementationOnce(cb => cb(null, true))
      .mockImplementationOnce(cb => cb(null, false));
    
    myMockFn((err, val) => console.log(val));
    // > true
    
    myMockFn((err, val) => console.log(val));
    // > false
    mock関数がmock Implementation Onceによって定義されたすべてのインプリメンテーションを使用する場合、jest.デフォルトのfnインプリメンテーションを実行します(定義されている場合)
    const myMockFn = jest
      .fn(() => 'default')
      .mockImplementationOnce(() => 'first call')
      .mockImplementationOnce(() => 'second call');
    
    console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
    // > 'first call', 'second call', 'default', 'default'
    Mock Names
    テストに失敗した場合、「jset.fn()」ではなく、エラー出力に表示されるmock関数の名前を選択的に指定することもできます.
    const myMockFn = jest
      .fn()
      .mockReturnValue('default')
      .mockImplementation(scalar => 42 + scalar)
      .mockName('add42');
    Custom Matchers
    参考https://jestjs.io/docs/expect!