次の(簡略化した)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で)戻りデータを指定します。
これは多かれ少なかれ、テストケースを次のように見せたいです。
解決時に事前定義されたテストデータを返すプロミスを返すように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();
});
_
時々、テストを書くのが難しいとき、それは私たちにデザインの問題があることを私たちに伝えようとしています。
小さなリファクタリングで物事がずっと簡単になると思います-SalesService
を内部ではなく共同作業者にします。
つまり、コンポーネント内でnew SalesService()
を呼び出す代わりに、呼び出しコードによって販売サービスを小道具として受け入れます。それを行う場合、呼び出しコードもテストになる可能性があります。この場合、必要なことはSalesService
自体をモックし、必要なものを返すことです(sinonまたは他のモックライブラリを使用して、または手巻きのスタブを作成するだけです)。
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();
});
私が過去に使用した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]また、ある時点で必要になる可能性がある、リモートサービスコールをシリアル化するための単一のポイントを提供するのにも役立ちます。