MockProviderはReact Apolloクライアントのリクエストや結果をモックするのに推奨されている方法です。サンプルコードを見る限りではいい感じに動くように見えますが、実際に使うとなると、結構ハマりどころがあります。MockProviderの使い方のヒントについてはすでにmediumなどに素晴らしい記事がありますが、それでもMockProvierの予期せぬ動作に数時間を溶かしてしまいました。
そんなわけでこの記事では、レスポンスを適切にモックするまでに得られた知見をシェアします。
なおテスト環境は以下です:
@apollo/client: 3.2.0 react-native:0.62.2 jest: 24.9.0 react-native-testing-library: 2.1.1
それでは行ってみましょう!
- 1. useQueryはhooks packageから直接インポートする
- 2. Fragmentを使っているときは__typenameも定義する
- 3. コンポーネントの描画を必ず走らせる
- 4. InMemoryCacheを挿入してエラーメッセージをわかりやすくする
- 5. "No more mocked responses" error => 引数をチェックする
- 6. Mutationや複数のQueryをテストする場合はキャッシュを無効化する
- まとめ
1. useQueryはhooks packageから直接インポートする
MockProvider で最初にハマったのがこちらのエラーメッセージ。
TypeError: (0 , _client.useQuery) is not a function > 35 | const { data } = useQuery(document, {
これは @apollo/client
から useQueryをインポートすると起きます。 @apollo/client/react/hooks/useQuery
から直接インポートしましょう。なお gql も同様に import { gql } from '@apollo/client/core';
とする必要があります。
import { useQuery } from '@apollo/client/react/hooks/useQuery'
2. Fragmentを使っているときは__typenameも定義する
上の記事でも触れられているのですが、よく引っかかるので、今一度強調しておきます。Fragment を使用している場合、gql に typename が定義されていなくても、モックされたレスポンスには必ず typename を追加しなければなりません。
例えば以下のようなクエリがあった場合。
query currentUserFriends( { me { id friends { pageInfo { ...pageInfoFragment } ... } } } fragment pageInfoFragment on PageInfo { endCursor hasNextPage hasPreviousPage startCursor }
モックデータは以下のようになります。
const mocks = [ { request: {... }, result: { data: { me: { id: '123', friends: { __typename: 'FriendConnection', pageInfo: { __typename: 'PageInfo', endCursor: '1', hasNextPage: false, hasPreviousPage: false, startCursor: '2', }, ... }, __typename: 'Me', }, }, }, }, ];
あともう一つ注意したいのが、Optional なフィールドであってもすべて何かしら定義する必要がある点です。
{| __typename: "Friend", id: string, name: string, age: ?number |}
上のようなフィールドがあったなら、モックデータは以下のように null でもよいので定義します。
{ __typename: "Friend", id: 3, name: "Buck", age: null }
3. コンポーネントの描画を必ず走らせる
MockProvider は、明示的にレンダリングをトリガーして更新を行わない限り、モックされたレスポンスを提供しません。react-native-testing-library を使用している場合は、act関数がコンポーネントを更新します。自分は以下のようなラッパーコンポーネントを作成しました。
const updateWrapper = async (wrapper, time = 10) => { await act(async () => { await new Promise(res => setTimeout(res, time)); }); };
テストする際は以下のようになります。
const { getByText } = render(component); await updateWrapper(component); expect(getByText('@Buck'));
4. InMemoryCacheを挿入してエラーメッセージをわかりやすくする
クエリのモックが失敗すると以下のようなメッセージがよく表示されます。
No instances found 96 | const { getByText } = render(component); 97 | await updateWrapper(component); > 98 | expect(getByText('@Buck'));
正直言って、このメッセージは全く役に立っていませんね。なぜ失敗しているのかわからない。より正確なエラーメッセージを得るためには、MockProviderのpropsとしてInMemoryCacheを追加してみるとよいです。
<MockedProvider mocks={mocks} addTypename cache={ new InMemoryCache({ possibleTypes, }) } > {component} </MockedProvider>
MockProvider であっても、Apollo は内部的に結果をキャッシュに書き込もうとします。このプロセスのあいだ、InMemoryCache は、クエリで Invariant Violation が見つかった場合にエラーを投げます。
InMemoryCache を追加した後、おそらく以下のようなエラーが表示されると思います。
Invariant Violation: Missing field 'node' in { "__typename": "FriendEdge", "cursor": "a" }
このエラー表示の方がわかりやすいですよね。あとは、エラーに表示されている、不足したフィールドを追加するだけです。
5. "No more mocked responses" error => 引数をチェックする
もうひとつよく見るエラーが"No more mocked responses"です。
このエラーが表示された場合、テストで渡されているクエリの変数が実際のコンポーネントと一致していないことが原因です。モックしている変数をよーくチェックする必要があります。
例を挙げてみましょう。以下のコンポーネントには { first:10 }
が入力されています。
const FriendList = (props: Props) => { const { data } = useQuery(document, { variables: { first: 10 }});
この場合はモックリクエストの variables
に同じ引数が渡っているか確認します。
const mocks = [ { request: { query: currentUserFriends, variables: { first: 10 } }, result: { ... } } ]
6. Mutationや複数のQueryをテストする場合はキャッシュを無効化する
キャッシュは Mutation や複数回実行される Query をテストする際に予期せぬ結果を引き起こす可能性があります。それに備えて MockProvider のキャッシュを無効にすることをお勧めします。
<MockedProvider mocks={mocks} addTypename defaultOptions={{ watchQuery: { fetchPolicy: 'no-cache' }, query: { fetchPolicy: 'no-cache' }, }} cache={ new InMemoryCache({ possibleTypes, }) } > {component} </MockedProvider>
まとめ
MockProvider は React Apollo テストを書く上で重要なツールですが、思った以上にドキュメントが少ないのが困ったところです。よくわからない問題に遭遇した場合は Github の issue でレポートが上がっているケースもあるので、そこを見に行くこともおすすめです。