Testing
Testing applications that are using Relay may be challenging, because of the additional data fetching layer that is wrapping 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 modules in relay-test-utils
that you may use in your tests:
createMockEnvironment(options): RelayMockEnvironment
MockPayloadGenerator
and the@relay_test_operation
directive
Use createMockEnvironment
to create an instance of RelayMockEnvironment
which implements the Relay Environment Interface and it also has an additional Mock layer, with methods that allow 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.
Most of GraphQL type information for a specific field in the selection is not available during Relay runtime. Using the @relay_test_operation
directive will add additional metadata containing GraphQL type info and it’ll improve the quality of the generated data.
For example, this test covers the Greetings
shown before.
import {act, render, waitFor} from '@testing-library/react';
import {Component, Suspense} from 'react';
import {RelayEnvironmentProvider} from 'react-relay';
import {
MockPayloadGenerator,
RelayMockEnvironment,
createMockEnvironment,
} from 'relay-test-utils';
import {Greetings} from './Greetings';
describe('<Greetings />', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
/** @type {RelayMockEnvironment} */
let environment;
class ErrorBoundary extends Component {
static getDerivedStateFromError(error) {
return {error};
}
state = {error: false};
render() {
const {children, fallback} = this.props;
const {error} = this.state;
return error ? fallback : children;
}
}
const TestRenderer = () => (
<RelayEnvironmentProvider environment={environment}>
<ErrorBoundary fallback="error">
<Suspense fallback="loading">
<Greetings />
</Suspense>
</ErrorBoundary>
</RelayEnvironmentProvider>
);
beforeEach(() => {
environment = createMockEnvironment();
});
afterEach(() => {
errorSpy.mockReset();
});
afterAll(() => {
errorSpy.mockRestore();
});
it('should handle loading state', () => {
const {getByText} = render(<TestRenderer />);
expect(getByText('loading')).toBeInTheDocument();
});
it('should handle data state with automatic mock', async () => {
const {getByText} = render(<TestRenderer />);
act(() => {
environment.mock.resolveMostRecentOperation((operation) =>
MockPayloadGenerator.generate(operation),
);
});
await waitFor(() => {
expect(
getByText('<mock-value-for-field-"greetings">'),
).toBeInTheDocument();
});
});
it('should handle data state with custom mock', async () => {
const {getByText} = render(<TestRenderer />);
act(() => {
environment.mock.resolveMostRecentOperation((operation) =>
MockPayloadGenerator.generate(operation, {
Query() {
return {
greetings: `Hello, ${operation.request.variables.name}!`,
};
},
}),
);
});
await waitFor(() => {
expect(getByText('Hello, Luke!')).toBeInTheDocument();
});
});
it('should handle error state', async () => {
const {getByText} = render(<TestRenderer />);
act(() => {
environment.mock.rejectMostRecentOperation(new Error('Uh-oh'));
});
await waitFor(() => {
expect(getByText('error')).toBeInTheDocument();
});
});
});
goto
script as a shortcutyarn goto playground/1-hello.3
For more information, read the docs here.