リレーコンポーネントのテスト
概要
このドキュメントの目的は、RelayコンポーネントをテストするためのRelay APIについて説明することです。
内容は主にjestの単体テスト(個々のコンポーネントのテスト)と統合テスト(コンポーネントの組み合わせのテスト)に焦点を当てています。ただし、これらのテストツールは、スクリーンショットテスト、本番環境の動作テスト、「Redbox」テスト、ファズテスト、e2eテストなど、さまざまなケースで適用できます。
jestテストを書くことの利点
- 一般的に、システムの安定性が向上します。FlowはさまざまなJavaScriptエラーを検出するのに役立ちますが、コンポーネントにリグレッションを導入する可能性は依然としてあります。単体テストは、リグレッションを見つけ、再現し、修正するのに役立ち、将来的にリグレッションを防ぎます。
- リファクタリングプロセスが簡素化されます。適切に記述されていれば(実装ではなくパブリックインターフェースをテスト)、テストはコンポーネントの内部実装の変更に役立ちます。
- 開発ワークフローを高速化および改善する可能性があります。一部の人はそれをテスト駆動開発(TDD)と呼ぶかもしれません。しかし、本質的には、コンポーネントのパブリックインターフェースのテストを作成し、次にそれらのインターフェースを実装するコンポーネントを作成するだけです。Jest —watchモードはこの場合に非常に役立ちます。
- 新しい開発者向けのオンボーディングプロセスが簡素化されます。テストがあることで、新しい開発者が新しいコードベースを迅速に理解し、バグを修正して機能を提供できるようになります。
注意すべきことの1つは、jestの単体テストおよび統合テストはシステムの安定性を向上させるのに役立ちますが、フロー、e2e、スクリーンショット、「Redbox」、パフォーマンステストなど、複数の自動テストレイヤーを備えた大規模な安定性インフラストラクチャの一部と見なす必要があることです。
Relayを使用したテスト
Relayを使用するアプリケーションのテストは、実際の製品コードをラップする追加のデータフェッチレイヤーがあるため、難しい場合があります。
また、Relayの背後で発生しているすべてのプロセスのメカニズムや、フレームワークとのインタラクションを適切に処理する方法を理解するのは必ずしも簡単ではありません。
幸いなことに、リクエスト/レスポンスフローを制御するための命令型APIと、モックデータ生成用の追加APIを提供することにより、Relayコンポーネントのテストを作成するプロセスを簡素化することを目的としたツールがあります。
テストで使用できる主なRelayモジュールは2つあります
createMockEnvironment(options):RelayMockEnvironment
MockPayloadGenerator
と@relay_test_operation
ディレクティブ
createMockEnvironment
を使用すると、テスト専用のRelay環境であるRelayMockEnvironment
のインスタンスを作成できます。createMockEnvironment
によって作成されたインスタンスは、Relay環境インターフェースを実装しており、さらに、操作(クエリ/ミューテーション/サブスクリプション)のフローを解決/拒否および制御できるメソッドを備えた追加のモックレイヤーも備えています。
MockPayloadGenerator
の主な目的は、テスト対象コンポーネントのモックデータを作成および維持するプロセスを改善することです。
Relayコンポーネントのテストでよく見られるパターンの1つは、テストコードの95%がテストの準備であるということです。つまり、ダミーデータを含む巨大なモックオブジェクトを手動で作成するか、ネットワーク応答として渡す必要があるサンプルサーバー応答のコピーを作成します。残りの5%が実際のテストコードです。その結果、人々はあまりテストしません。さまざまなケースに対応するこれらのすべてのダミーペイロードを作成および管理するのは困難です。したがって、テストの作成には時間がかかり、テストの維持が困難になる場合があります。
MockPayloadGenerator
と@relay_test_operation
を使用すると、このパターンをなくし、開発者の焦点をテストの準備から実際のテストに切り替えたいと考えています。
ReactとRelayを使用したテスト
React Testing Libraryは、Reactコンポーネントの実装の詳細に依存せずにテストできる一連のヘルパーです。このアプローチにより、リファクタリングが容易になり、アクセシビリティのベストプラクティスに促されます。子を持たないコンポーネントを「浅く」レンダリングする方法は提供されていませんが、Jestのようなテストランナーでは、モックすることでこれを行うことができます。
RelayMockEnvironment API概要
RelayMockEnvironmentは、操作フローを制御するための追加のAPIメソッド(操作の解決と拒否、サブスクリプションの増分ペイロードの提供、キャッシュの操作など)を備えたRelay環境の特別なバージョンです。
- 環境で実行された操作を検索するためのメソッド
getAllOperations()
- 現在のテスト中に実行されたすべての操作を取得しますfindOperation(findFn => boolean)
- 実行されたすべての操作のリストから特定の操作を検索します。操作が利用できない場合はこのメソッドはスローします。複数の操作が同時に実行された場合に、特定の操作を検索するのに役立ちますgetMostRecentOperation() -
最新の操作を返します。このメソッドは、この呼び出しの前に操作が実行されなかった場合はスローします。
- 操作を解決または拒否するためのメソッド
nextValue(request | operation, data)
- 操作(リクエスト)のペイロードを提供しますが、リクエストは完了しません。実際には、増分更新とサブスクリプションをテストする場合に役立ちますcomplete(request | operation)
- 操作を完了します。操作が完了すると、この操作にこれ以上のペイロードは期待されません。resolve(request | operation, data)
- 提供されたGraphQL応答でリクエストを解決します。基本的には、nextValue(...)とcomplete(...)です。reject(request | operation, error)
- 特定のエラーでリクエストを拒否しますresolveMostRecentOperation(operation => data)
- resolveとgetMostRecentOperationが連携して動作しますrejectMostRecentOperation(operation => error)
- rejectとgetMostRecentOperationが連携して動作しますqueueOperationResolver(operation => data | error)
- キューにOperationResolver関数を追加します。渡されたリゾルバーは、操作が表示されたときに操作を解決/拒否するために使用されますqueuePendingOperation(query, variables)
-usePreloadedQuery
フックをサスペンドさせないためには、これらの関数を呼び出す必要がありますqueueOperationResolver(resolver)
queuePendingOperation(query, variables)
preloadQuery(mockEnvironment, query, variables)
、queuePendingOperation
に渡されたものと同じquery
とvariables
を使用します。preloadQuery
は、queuePendingOperation
の後に呼び出す必要があります。
- 追加のユーティリティメソッド
isLoading(request | operation)
- 操作がまだ完了していない場合はtrue
を返します。cachePayload(request | operation, variables, payload)
- ペイロードをQueryResponseキャッシュに追加しますclearCache()
- QueryResponseキャッシュをクリアします
モックペイロードジェネレーターと@relay_test_operation
ディレクティブ
MockPayloadGenerator
は、テスト用のモックデータを作成および維持するプロセスを大幅に簡素化できます。MockPayloadGenerator
は、操作内の選択に基づいてダミーデータを生成できます。生成されたデータを変更するためのAPI(モックリゾルバー)があります。モックリゾルバーを使用すると、ニーズに合わせてデータを調整できます。モックリゾルバーは、キーがGraphQL型の名前(ID
、String
、User
、Comment
など)であり、値が型のデフォルトデータを返す関数であるオブジェクトとして定義されます。
簡単なモックリゾルバーの例
{
ID() {
// Return mock value for a scalar filed with type ID
return 'my-id';
},
String() {
// Every scalar field with type String will have this default value
return "Lorem Ipsum"
}
}
オブジェクト型に対してより多くのリゾルバーを定義できます
{
// This will be the default values for User object in the query response
User() {
return {
id: 4,
name: "Mark",
profile_picture: {
uri: "http://my-image...",
},
};
},
}
モックリゾルバーコンテキスト
MockResolverの最初の引数は、Mock Resolver Contextを含むオブジェクトです。コンテキストに基づいてモックリゾルバーから動的な値を返すことができます。たとえば、フィールドの名前またはエイリアス、選択範囲内のパス、引数、または親の型などです。
{
String(context) {
if (context.name === 'zip') {
return '94025';
}
if (context.path != null && context.path.join('.') === 'node.actor.name') {
return 'Current Actor Name';
}
if (context.parentType === 'Image' && context.name === 'uri') {
return 'http://my-image.url';
}
}
}
ID生成
モックリゾルバーの2番目の引数は、テストで一意のIDを生成するのに役立つ整数のシーケンスを生成する関数です
{
// will generate strings "my-id-1", "my-id-2", etc.
ID(_, generateId) {
return `my-id-${generateId()}`;
},
}
Float、Integer、Booleanなど...
本番環境のクエリでは、Boolean、Integer、Floatなどのスカラーフィールドの完全な型情報がないことに注意してください。また、MockResolversでは、これらはStringにマップされます。context
を使用して、フィールド名、エイリアスなどに基づいて戻り値を調整できます。
@relay_test_operation
選択内の特定のフィールドのGraphQL型情報のほとんどは、Relayランタイムでは利用できません。デフォルトでは、Relayは、選択内のスカラーフィールドやオブジェクトのインターフェース型の型情報を取得できません。
@relay_test_operationディレクティブを含む操作には、操作の選択範囲のフィールドのGraphQL型情報を含む追加のメタデータが含まれます。また、生成されたデータの品質が向上します。スカラー(IDとStringだけでなく)および抽象型に対してモックリゾルバーを定義することもできます
{
Float() {
return 123.456;
},
Boolean(context) {
if (context.name === 'can_edit') {
return true;
}
return false;
},
Node() {
return {
__typename: 'User',
id: 'my-user-id',
};
}
}
例
Relayコンポーネントテスト
createMockEnvironment
とMockPayloadGenerator
を使用すると、Relayフックを使用するコンポーネントの簡潔なテストを作成できます。これらのモジュールはどちらもrelay-test-utils
からインポートできます
// Say you have a component with the useLazyLoadQuery or a QueryRenderer
const MyAwesomeViewRoot = require('MyAwesomeViewRoot');
const {
createMockEnvironment,
MockPayloadGenerator,
} = require('relay-test-utils');
const {act, render} = require('@testing-library/react');
// Relay may trigger 3 different states
// for this component: Loading, Error, Data Loaded
// Here is examples of tests for those states.
test('Loading State', async () => {
const environment = createMockEnvironment();
const renderer = render(
<MyAwesomeViewRoot environment={environment} />,
);
// Here we just verify that the spinner is rendered
expect(await renderer.findByTestId('spinner')).toBeDefined();
});
test('Data Render', async () => {
const environment = createMockEnvironment();
const renderer = render(
<MyAwesomeViewRoot environment={environment} />,
);
// Wrapping in act will ensure that components
// are fully updated to their final state.
act(() => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation),
);
});
// At this point operation will be resolved
// and the data for a query will be available in the store
expect(await renderer.findByTestId('myButton')).toBeDefined();
});
test('Error State', async () => {
const environment = createMockEnvironment();
const renderer = render(
<MyAwesomeViewRoot environment={environment} />,
);
// Wrapping in act will ensure that components
// are fully updated to their final state.
act(() => {
// Error can be simulated with `rejectMostRecentOperation`
environment.mock.rejectMostRecentOperation(new Error('Uh-oh'));
});
expect(await renderer.findByTestId('errorMessage')).toBeDefined();
});
遅延フラグメントを含むコンポーネントテスト
MockPayloadGenerator
を使用して、@defer
を含むフラグメントを持つクエリのデータを生成する場合、遅延データも生成できます。そのためには、generateDeferredPayload
オプションを渡してMockPayloadGenerator.generateWithDefer
を使用できます
// Say you have a component with useFragment
const ChildComponent = (props: {user: ChildComponentFragment_user$key}) => {
const data = useFragment(graphql`
fragment ChildComponentFragment_user on User {
name
}
`, props.user);
return <View>{data?.name}</View>;
};
// Say you have a parent component that fetches data with useLazyLoadQuery and `@defer`s the data for the ChildComponent.
const ParentComponent = () => {
const data = useLazyLoadQuery(graphql`
query ParentComponentQuery {
user {
id
...ChildComponentFragment_user @defer
}
}
`, {});
return (
<View>
{id}
<Suspense fallback={null}>
{data?.user && <ChildComponent user={data.user} />}
</Suspense>
</View>
);
};
const {
createMockEnvironment,
MockPayloadGenerator,
} = require('relay-test-utils');
const {act, render} = require('@testing-library/react');
test('Data Render with @defer', () => {
const environment = createMockEnvironment();
const renderer = render(
<RelayEnvironmentProvider environment={environment}>
<ParentComponent />,
</RelayEnvironmentProvider>
);
// Wrapping in ReactTestRenderer.act will ensure that components
// are fully updated to their final state.
act(() => {
const operation = environment.mock.getMostRecentOperation();
const mockData = MockPayloadGenerator.generateWithDefer(operation, null, {generateDeferredPayload: true});
environment.mock.resolve(mockData);
// You may need this to make sure all payloads are retrieved
jest.runAllTimers();
});
// At this point operation will be resolved
// and the data for a query will be available in the store
expect(renderer.container.textContent).toEqual(['id', 'name']);
});
フラグメントコンポーネントテスト
基本的に、上記の例では、resolveMostRecentOperation
はすべての子フラグメントコンテナ(ページネーション、リフェッチ)のデータを生成します。しかし、通常、ルートコンポーネントには多くの子フラグメントコンポーネントがあり、useFragment
を使用する特定のコンポーネントをテストしたい場合があります。その解決策は、フラグメントコンポーネントからフラグメントを広げるQueryをレンダリングするuseLazyLoadQuery
コンポーネントでフラグメントコンテナをラップすることです。
test('Fragment', () => {
const environment = createMockEnvironment();
const TestRenderer = () => {
const data = useLazyLoadQuery(
graphql`
query TestQuery @relay_test_operation {
myData: node(id: "test-id") {
# Spread the fragment you want to test here
...MyFragment
}
}
`,
{},
);
return <MyFragmentComponent myData={data.myData} />
};
const renderer = render(
<RelayEnvironmentProvider environment={environment}>
<Suspense fallback="Loading...">
<TestRenderer />
</Suspense>
</RelayEnvironmentProvider>
);
// Wrapping in act will ensure that components
// are fully updated to their final state.
act(() => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation),
);
});
expect(renderer).toMatchSnapshot();
});
ページネーションコンポーネントのテスト
基本的に、ページネーションコンポーネント(例:usePaginationFragment
を使用)のテストは、フラグメントコンポーネントのテストと変わりません。しかし、ここではさらに多くのことができます。実際にページネーションがどのように機能するかを確認できます。ページネーション(さらに読み込む、リフェッチ)を実行する際のコンポーネントの動作をアサートできます。
// Pagination Example
test('`Pagination` Container', async () => {
const environment = createMockEnvironment();
const TestRenderer = () => {
const data = useLazyLoadQuery(
graphql`
query TestQuery @relay_test_operation {
myConnection: node(id: "test-id") {
connection {
# Spread the pagination fragment you want to test here
...MyConnectionFragment
}
}
}
`,
{},
);
return <MyPaginationContainer connection={data.myConnection.connection} />
};
const renderer = render(
<RelayEnvironmentProvider environment={environment}>
<Suspense fallback="Loading...">
<TestRenderer />
</Suspense>
</RelayEnvironmentProvider>
);
// Wrapping in act will ensure that components
// are fully updated to their final state.
act(() => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation, {
ID(_, generateId) {
// Why we're doing this?
// To make sure that we will generate a different set of ID
// for elements on first page and the second page.
return `first-page-id-${generateId()}`;
},
PageInfo() {
return {
has_next_page: true,
};
},
}),
);
});
// Let's find a `loadMore` button and click on it to initiate pagination request, for example
const loadMore = await renderer.findByTestId('loadMore');
expect(loadMore.props.disabled).toBe(false);
loadMore.props.onClick();
// Wrapping in act will ensure that components
// are fully updated to their final state.
act(() => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation, {
ID(_, generateId) {
// See, the second page IDs will be different
return `second-page-id-${generateId()}`;
},
PageInfo() {
return {
// And the button should be disabled, now. Probably.
has_next_page: false,
};
},
}),
);
});
expect(loadMore.props.disabled).toBe(true);
});
リフェッチコンポーネント
ここでも、クエリでコンポーネントをラップするという同様のアプローチを使用できます。完全を期すために、ここに例を追加します。
test('Refetch Container', async () => {
const environment = createMockEnvironment();
const TestRenderer = () => {
const data = useLazyLoadQuery(
graphql`
query TestQuery @relay_test_operation {
myData: node(id: "test-id") {
# Spread the pagination fragment you want to test here
...MyRefetchableFragment
}
}
`,
{},
);
return <MyRefetchContainer data={data.myData} />
};
const renderer = render(
<RelayEnvironmentProvider environment={environment}>
<Suspense fallback="Loading...">
<TestRenderer />
</Suspense>
</RelayEnvironmentProvider>
);
act(() => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation),
);
});
// Assuming we have refetch button in the Container
const refetchButton = await renderer.findByTestId('refetch');
// This should trigger the `refetch`
refetchButton.props.onClick();
act(() => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation, {
// We can customize mock resolvers, to change the output of the refetch query
}),
);
});
expect(renderer).toMatchSnapshot();
});
ミューテーション
ミューテーション自体が操作であるため、特定のミューテーションに対して独立して(単体テスト)、またはこのミューテーションが呼び出されるビューと組み合わせてテストできます。
useMutation
APIは、commitMutation
を直接呼び出すよりも改善されています。
// Say, you have a mutation function
function sendMutation(environment, onCompleted, onError, variables)
commitMutation(environment, {
mutation: graphql`...`,
onCompleted,
onError,
variables,
});
}
// Example test may be written like so
test('it should send mutation', () => {
const environment = createMockEnvironment();
const onCompleted = jest.fn();
sendMutation(environment, onCompleted, jest.fn(), {});
const operation = environment.mock.getMostRecentOperation();
act(() => {
environment.mock.resolve(
operation,
MockPayloadGenerator.generate(operation)
);
});
expect(onCompleted).toBeCalled();
});
サブスクリプション
useSubscription
APIは、requestSubscription
を直接呼び出すよりも改善されています。
サブスクリプションは、ミューテーションをテストする方法と同様にテストできます。
// Example subscribe function
function subscribe(environment, onNext, onError, variables)
requestSubscription(environment, {
subscription: graphql`...`,
onNext,
onError,
variables,
});
}
// Example test may be written like so
test('it should subscribe', () => {
const environment = createMockEnvironment();
const onNext = jest.fn();
subscribe(environment, onNext, jest.fn(), {});
const operation = environment.mock.getMostRecentOperation();
act(() => {
environment.mock.nextValue(
operation,
MockPayloadGenerator.generate(operation)
);
});
expect(onNext).toBeCalled();
});
queueOperationResolver
の例
queueOperationResolver
を使用すると、環境で実行される操作に対するレスポンスを定義できます。
// Say you have a component with the QueryRenderer
const MyAwesomeViewRoot = require('MyAwesomeViewRoot');
const {
createMockEnvironment,
MockPayloadGenerator,
} = require('relay-test-utils');
test('Data Render', async () => {
const environment = createMockEnvironment();
environment.mock.queueOperationResolver(operation =>
MockPayloadGenerator.generate(operation),
);
const renderer = render(
<MyAwesomeViewRoot environment={environment} />,
);
// At this point operation will be resolved
// and the data for a query will be available in the store
expect(await renderer.findByTestId('myButton')).toBeDefined();
});
test('Error State', async () => {
const environment = createMockEnvironment();
environment.mock.queueOperationResolver(() =>
new Error('Uh-oh'),
);
const renderer = render(
<MyAwesomeViewRoot environment={environment} />,
);
expect(await renderer.findByTestId('myButton')).toBeDefined();
});
Relay Hooksを使用
このガイドの例は、Relay Hooks、コンテナ、レンダラーの両方でコンポーネントをテストするために機能するはずです。usePreloadedQuery
フックを含むテストを作成する場合は、上記のqueuePendingOperation
の注意も参照してください。
toMatchSnapshot(...)
ここにあるすべての例では、toMatchSnapshot()
を使用したアサーションが表示されますが、例を簡潔にするためにそのままにしています。しかし、これはコンポーネントをテストするための推奨される方法ではありません。
その他の例
テスト例の最良のソースは、relay-experimentalパッケージにあります。
テストは良いことです。必ず行うべきです。
このページは役に立ちましたか?
いくつかの簡単な質問に答えて、サイトをさらに良くするのにご協力ください いくつかの簡単な質問に答えてください.