Testing Relay with Preloaded Queries
Components that use preloaded queries (useQueryLoader
and usePreloadedQuery
hooks) require slightly different and more convoluted test setup.
In short, there are two steps that need to be performed before rendering the component
- Configure the query resolver to generate the response via
environment.mock.queueOperationResolver
- Record a pending queue invocation via
environment.mock.queuePendingOperation
Symptoms that something is wrong​
- The test doesn't do what is expected from it.
- The query seems to be blocking instead of executing
- E.g. the
Suspend
doesn't switch from "waiting" to "data loaded" state
- E.g. the
- If you add the
console.log
before and afterusePreloadedQuery
, only the "before" call is hit
TL;DR​
const {RelayEnvironmentProvider} = require('react-relay');
const { MockPayloadGenerator, createMockEnvironment } = require('relay-test-utils');
const {render} = require('testing-library-react');
// at the time of writing, act is not re-exported by our internal testing-library-react
// but is re-exported by the "external" version
const {act} = require('ReactTestUtils');
test("...", () => {
// arrange
const environment = createMockEnvironment();
environment.mock.queueOperationResolver(operation => {
return MockPayloadGenerator.generate(operation, {
CurrencyAmount() {
return {
formatted_amount: "1234$",
};
},
});
});
const query = YourComponentGraphQLQueryGoesHere; // can be the same, or just identical
const variables = {
// ACTUAL variables for the invocation goes here
};
environment.mock.queuePendingOperation(YourComponentGraphQLQuery, variables);
// act
const {getByTestId, ..otherStuffYouMightNeed} = render(
<RelayEnvironmentProvider environment={environment}>
<YourComponent data-testid="1234" {...componentPropsIfAny}/>
</RelayEnvironmentProvider>
);
// trigger the loading - click a button, emit an event, etc. or ...
act(() => jest.runAllImmediates()); // ... if loadQuery is in the useEffect()
// assert
// your assertions go here
});
Configure the query resolver to generate the response​
This is done via environment.mock.queueOperationResolver(operation)
call, but getting it right might be tricky.
The crux of this call is to return a mocked graphql result in a very particular format (as MockResolvers
type, to be precise). This is done via a second parameter to generate
- it is an object, whose keys are GraphQL types that we want to mock. (See mock-payload-generator
).
Continuing on the above example:
return MockPayloadGenerator.generate(operation, {
CurrencyAmount() { // <-- the GraphQL type
return {
formatted_amount: "response_value" <-- CurrencyAmount fields, selected in the query
};
}
});
The tricky thing here is to obtain the name of the GraphQL type and fields to return. This can be done in two ways:
- Call
console.log(JSON.stringify(operation, null, 2))
and look for theconcreteType
that corresponds to what we want to mock. Then look at the siblingselections
array, which describes the fields that are selected from that object.
It is possible to return different data for different query variables via Mock Resolver Context. The query variables will be available on the context.args
, but only to the innermost function call (for the query above, only offer_ids
are available)
CurrencyAmount(context) {
console.log(JSON.stringify(context, null, 2)); // <--
return { formatted_amount: mockResponse }
}
// <-- logs { ...snip..., "name": "subtotal_price_for_offers", args: { offer_ids: [...] } }
Record a pending queue invocation​
This is more straightforward - it is done via a call to environment.mock.queuePendingOperation(query, variables)
Query
needs to match the query issues by the component. Simplest (and most robust against query changes) is to export the query from the component module and use it in the test, but having an identical (but not the same) query works as well.variables
has to match the variables that will be used in this test invocation.- Beware of nested objects and arrays - they are compared via
areEqual
(invocation code)- Arrays are compared by values (not by reference), but the order of elements matter
- Nested objects - performs deep compare, order of keys is not relevant (this is not confirmed - please update this doc if you used a graphql query with "deep" structure)
- Beware of nested objects and arrays - they are compared via
Troubleshooting​
console.log
,console.log
everywhere! Recommended places:- component: before and after
useQueryLoader, usePreloadedQuery, loadQuery
- test: in
queueOperationResolver
callback - library: in
RelayModernMockEnvironment.execute
, after theconst currentOperation = ...
call (here)
- component: before and after
- If
loadQuery
is not called - make sure to issue the triggering event. Depending on your component implementation it could be a user-action (like button click or key press), javascript event (via event emitter mechanisms) or a simple "delayed execution" withuseEffect
.- The
useEffect
case is probably easiest to miss - make sure to callact(() => jest.runAllImmediates())
after rendering the component
- The
- If "before"
usePreloadedQuery
is hit, but "after" is not - the query suspends. This entire guide is written to resolve it - you might want to re-read it. But most likely it is either:- Used a different query - the query resolver would not be called,
currentOperation
will benull
- Query variables don't match - the query resolver would not be called,
currentOperation
will benull
(make sure to inspect thevariables
).- Also, make sure arrays are in the same order, if any (or better yet, use sets, if at all possible).
- Used a different query - the query resolver would not be called,
- If data returned rom the query is not what you expect, make sure you're generating the right graphql type.
- You can tell you're mocking the wrong one if the return values look something like
<mock-value-for-field-"formatted_amount">
- You can tell you're mocking the wrong one if the return values look something like
Make sure the component and the test use the same environment (i.e. there's no <RelayEnvironmentProvider environment={RelayFBEnvironment}>
somewhere nested in your test React tree.
Epilogue​
Examples here use testing-library-react
, but it works with the react-test-renderer
as well.
Is this page useful?
Help us make the site even better by answering a few quick questions.