Testing Relay Components
Abstract​
The purpose of this document is to cover the Relay APIs for testing Relay components.
The content is focused mostly on jest unit-tests (testing individual components) and integration tests (testing a combination of components). But these testing tools may be applied in different cases: screenshot-tests, production smoke-tests, "Redbox" tests, fuzz-tests, e2e test, etc.
What are the benefits of writing jest tests:
- In general, it improves the stability of the system. Flow helps with catching a various set of Javascript errors, but it is still possible to introduce regressions to the components. Unit-tests help find, reproduce, and fix regressions, and prevent them in the future.
- It simplifies the refactoring process: when properly written (testing public interface, not implementation) - tests help with changing the internal implementation of the components.
- It may speed up and improve the development workflow. Some people may call it Test Driven Development (TM). But essentially it's just writing tests for public interfaces of your components, and then writing the components that implement those interfaces. Jest —watch mode really shines in this case.
- It will simplify the on-boarding process for new developers. Having tests helps new developers ramp up on the new code base, allowing them to fix bugs and deliver features.
One thing to notice: while jest unit- and integration tests will help improve the stability of the system, they should be considered one part of a bigger stability infrastructure with multiple layers of automated testing: flow, e2e, screenshot, "Redbox", performance tests.
Testing with Relay​
Testing applications that use Relay may be challenging, because of the additional data fetching layer that wraps the actual product code.
And it's not always easy to understand the mechanics of all processes that are happening behind Relay, and how to properly handle interactions with the framework.
Fortunately, there are tools that aim to simplify the process of writing tests for Relay components, by providing imperative APIs for controlling the request/response flow and additional API for mock data generation.
There are two main Relay modules that you may use in your tests:
createMockEnvironment(options): RelayMockEnvironmentMockPayloadGeneratorand the@relay_test_operationdirective
With createMockEnvironment, you will be able to create an instance of RelayMockEnvironment, a Relay environment specifically for your tests. The instance created by createMockEnvironment implements the Relay Environment Interface and it also has an additional Mock layer, with methods that allow you to resolve/reject and control the flow of operations (queries/mutations/subscriptions).
The main purpose of MockPayloadGenerator is to improve the process of creating and maintaining the mock data for tested components.
One of the patterns you may see in the tests for Relay components: 95% of the test code is the test preparation—the gigantic mock object with dummy data, manually created, or just a copy of a sample server response that needs to be passed as the network response. And the remaining 5% is actual test code. As a result, people don't test much. It's hard to create and manage all these dummy payloads for different cases. Hence, writing tests is time-consuming and tests are sometimes painful to maintain.
With the MockPayloadGenerator and @relay_test_operation, we want to get rid of this pattern and switch the developer's focus from the preparation of the test to the actual testing.
Testing with React and Relay​
React Testing Library is a set of helpers that let you test React components without relying on their implementation details. This approach makes refactoring a breeze and also nudges you towards best practices for accessibility. Although it doesn't provide a way to "shallowly" render a component without its children, a test runner like Jest lets you do this by mocking.
RelayMockEnvironment API Overview​
RelayMockEnvironment is a special version of Relay Environment with additional API methods for controlling the operation flow: resolving and rejection operations, providing incremental payloads for subscriptions, working with the cache.
- Methods for finding operations executed on the environment
getAllOperations()- get all operation executed during the test by the current timefindOperation(findFn => boolean)- find particular operation in the list of all executed operations, this method will throw, if operation is not available. Maybe useful to find a particular operation when multiple operations executed at the same timegetMostRecentOperation() -return the most recent operation, this method will throw if no operations were executed prior this call.
- Methods for resolving or rejecting operations
nextValue(request | operation, data)- provide payload for operation(request), but not complete request. Practically useful when testing incremental updates and subscriptionscomplete(request | operation)- complete the operation, no more payloads are expected for this operation, when it's completed.resolve(request | operation, data)- resolve the request with provided GraphQL response. Essentially, it's nextValue(...) and complete(...)reject(request | operation, error)- reject the request with particular errorresolveMostRecentOperation(operation => data)- resolve and getMostRecentOperation work togetherrejectMostRecentOperation(operation => error)- reject and getMostRecentOperation work togetherqueueOperationResolver(operation => data | error)- adds an OperationResolver function to the queue. The passed resolver will be used to resolve/reject operations as they appearqueuePendingOperation(query, variables)- in order for theusePreloadedQueryhook to not suspend, one must call these functions:queueOperationResolver(resolver)queuePendingOperation(query, variables)preloadQuery(mockEnvironment, query, variables)with the samequeryandvariablesthat were passed toqueuePendingOperation.preloadQuerymust be called afterqueuePendingOperation.
- Additional utility methods
isLoading(request | operation)- will returntrueif operations has not been completed, yet.cachePayload(request | operation, variables, payload)- will add payload to QueryResponse cacheclearCache()- will clear QueryResponse cache
Mock Payload Generator and the @relay_test_operation Directive​
MockPayloadGenerator may drastically simplify the process of creating and maintaining mock data for your tests. MockPayloadGenerator can generate dummy data for the selection that you have in your operation. There is an API to modify the generated data - Mock Resolvers. With Mock Resolvers, you may adjust the data for your needs. Mock Resolvers are defined as an object where keys are names of GraphQL types (ID, String, User, Comment, etc), and values are functions that return the default data for the type.
Example of a simple Mock Resolver:
{
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"
}
}
It is possible to define more resolvers for Object types
{
// 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...",
},