web-dev-qa-db-ja.com

別のクラスで非同期関数呼び出しをモックする方法

次の(簡略化した)Reactコンポーネントがあります。

_class SalesView extends Component<{}, State> {
  state: State = {
    salesData: null
  };

  componentDidMount() {
    this.fetchSalesData();
  }

  render() {
    if (this.state.salesData) {
      return <SalesChart salesData={this.state.salesData} />;
    } else {
      return <p>Loading</p>;
    }
  }

  async fetchSalesData() {
    let data = await new SalesService().fetchSalesData();
    this.setState({ salesData: data });
  }
}
_

マウントするときに、SalesServiceというクラスで抽象化したAPIからデータをフェッチします。このクラスをモックしたいのですが、メソッドfetchSalesDataの場合は、(promiseで)戻りデータを指定します。

これは多かれ少なかれ、テストケースを次のように見せたいです。

  • テストデータを事前定義
  • salesViewをインポートする
  • モックSalesService
  • 解決時に事前定義されたテストデータを返すプロミスを返すようにmockSalesServiceを設定する

  • コンポーネントを作成する

  • 待つ
  • スナップショットを確認する

SalesChartの外観のテストはこの質問の一部ではありません。Enzymeを使用して解決することを望みます。私はこの非同期呼び出しを模倣するために何十ものことを試みてきましたが、これを適切に模倣することができないようです。オンラインでJestをモックする次の例を見つけましたが、この基本的な使い方をカバーしていないようです。

私の質問は:

  • モッククラスはどのように見えるべきですか?
  • このモッククラスはどこに配置すればよいですか?
  • このモッククラスをインポートするにはどうすればよいですか?
  • このモッククラスが実際のクラスを置き換えることをどのように確認できますか?
  • モッククラスの特定の関数のモック実装をセットアップするにはどうすればよいですか?
  • 約束が解決されるのをテストケースで待機するにはどうすればよいですか?

私が持っている、うまくいかない一例を以下に示します。テストランナーがエラー_throw err;_でクラッシュし、スタックトレースの最後の行はat process._tickCallback (internal/process/next_tick.js:188:7)です

_# __tests__/SalesView-test.js
import React from 'react';
import SalesView from '../SalesView';

jest.mock('../SalesService');
const salesServiceMock = require('../SalesService').default;

const weekTestData = [];

test('SalesView shows chart after SalesService returns data', async () => {
  salesServiceMock.fetchSalesData.mockImplementation(() => {
    console.log('Mock is called');
    return new Promise((resolve) => {
      process.nextTick(() => resolve(weekTestData));
    });
  });

  const wrapper = await shallow(<SalesView/>);
  expect(wrapper).toMatchSnapshot();
});
_
21

時々、テストを書くのが難しいとき、それは私たちにデザインの問題があることを私たちに伝えようとしています。

小さなリファクタリングで物事がずっと簡単になると思います-SalesServiceを内部ではなく共同作業者にします。

つまり、コンポーネント内でnew SalesService()を呼び出す代わりに、呼び出しコードによって販売サービスを小道具として受け入れます。それを行う場合、呼び出しコードもテストになる可能性があります。この場合、必要なことはSalesService自体をモックし、必要なものを返すことです(sinonまたは他のモックライブラリを使用して、または手巻きのスタブを作成するだけです)。

12
Kraylog

SalesService.create()メソッドを使用してnewキーワードを抽象化し、次に jest.spyOn(object、methodName) を使用して実装をモックすることができます。

import SalesService from '../SalesService ';

test('SalesView shows chart after SalesService returns data', async () => {

    const mockSalesService = {
        fetchSalesData: jest.fn(() => {
            return new Promise((resolve) => {
                process.nextTick(() => resolve(weekTestData));
            });
        })
    };

    const spy = jest.spyOn(SalesService, 'create').mockImplementation(() => mockSalesService);

    const wrapper = await shallow(<SalesView />);
    expect(wrapper).toMatchSnapshot();
    expect(spy).toHaveBeenCalled();
    expect(mockSalesService.fetchSalesData).toHaveBeenCalled();

    spy.mockReset();
    spy.mockRestore();
});
5
Jake Holzinger

私が過去に使用した1つの「醜い」方法は、一種の貧乏人の依存関係注入を行うことです。

これは、必要になるたびに実際にSalesServiceをインスタンス化する必要はないかもしれないという事実に基づいています。むしろ、アプリケーションごとに1つのインスタンスを保持し、誰もが使用するようにしたいという事実に基づいています。私の場合、SalesServiceには毎回繰り返したくない初期設定が必要でした。[1]

だから私がしたことはservices.tsファイルは次のようになります。

/// In services.ts
let salesService: SalesService|null = null;
export function setSalesService(s: SalesService) {
    salesService = s;
}
export function getSalesService() {
    if(salesService == null) throw new Error('Bad stuff');
    return salesService;
}

次に、私のアプリケーションのindex.tsxまたは私が持っているのと同じような場所:

/// In index.tsx
// initialize stuff
const salesService = new SalesService(/* initialization parameters */)
services.setSalesService(salesService);
// other initialization, including calls to React.render etc.

コンポーネントでは、getSalesServiceを使用して、アプリケーションごとに1つのSalesServiceインスタンスへの参照を取得できます。

テストするときは、mocha(または何でも)beforeまたはbeforeEachハンドラーでいくつかの設定を行ってsetSalesServiceを呼び出し、モックオブジェクト。

ここで、理想的には、SalesServiceをコンポーネントへのプロップとして渡します。これは、がコンポーネントへの入力であるためgetSalesServiceを使用することで、この依存関係を隠し、恐らくあなたを悲しませます。しかし、非常にネストされたコンポーネントでそれが必要な場合、またはルーターなどを使用している場合、それをプロップとして渡すのは非常に扱いにくくなります。

context のようなものを使用して、すべてをReactのままにしておくこともできます。

これに対する「理想的な」解決策は、依存性注入のようなものですが、React AFAIK。


[1]また、ある時点で必要になる可能性がある、リモートサービスコールをシリアル化するための単一のポイントを提供するのにも役立ちます。

2
Horia Coman