DEV Community

Augusto Calaca
Augusto Calaca

Posted on

How to test your relay components with relay-test-utils and react-testing-library

This blogpost is an improvement over a thread I did on twitter some time ago.

Now I show a similar example but with the use of relay hooks and a little more information.

If you need to refactor code and have no tests to offer you coverage and security, unfortunately, you will be in trouble. Tests really improves the stability of the codebase and helps with changing the internal implementation of the components.

Relay-test-utils

Testing applications that are using relay may be challenging, but the relay-test-utils makes things a lot easier.
It provides imperative APIs for controlling the request/response flow and additional API for mock data generation.

There are two main modules that you will enjoy using in your tests:

  • createMockEnvironment
  • mockPayloadGenerator

The createMockEnvironment nothing more than an implementation the Relay Environment Interface and it also has an additional mock layer, with methods that allow resolving/reject and control of operations (queries/mutations/subscriptions).

The mockPayloadGenerator is to improve the process of creating and maintaining the mock data for tested components
it can generate dummy data for the selection that you have in your operation.

Consider that we want to test if this transaction listing goes as expected:

The code above use the hooks useLazyLoadQuery and usePaginationFragment to load a transactions list in which each has the user who is sending fromUser, the user who is receiving toUser, the value, the cashback (5% of the value) and any message.

RelayMockEnvironment

CreateMockEnvironment is a special version of Relay Environment with an additional API methods for controlling the resolving and rejection operations. Therefore the first thing we should do is tell jest that we don't want the default environment but our environment provided by relay-test-utils.


jest.mock('path/to/Environment', () => {
  const { createMockEnvironment } = require('relay-test-utils');
  return createMockEnvironment();
});

Enter fullscreen mode Exit fullscreen mode

The relay team is very concerned with reducing the preparation time of the test environment so that the developer can focus on actually testing its components. That way, from there we can already test our component.

With React-testing-library

Reject

Here we use object destructuring to capture the value of getByText from our render() function.
Let's use rejectMostRecentOperation first and we check if the component raises the error or not. In the first tests, I always like to test cases where the component may fail.

import React from 'react';
import { render, cleanup } from '@testing-library/react';
import { MockPayloadGenerator } from 'relay-test-utils';
import { useRelayEnvironment } from 'react-relay/hooks';
import TransactionList from '../TransactionList';

afterEach(cleanup);

it('should reject query', () => {
  const Environment = useRelayEnvironment();
  const { getByText } = render(<TransactionList />);

  Environment.mock.rejectMostRecentOperation(new Error('A very bad error'));
  expect(getByText('Error: A very bad error')).toBeTruthy();
});

Enter fullscreen mode Exit fullscreen mode

We can also check if the error occurs in the expected operation.


it('should reject query with function and render error with name of the operation', () => {
    const Environment = useRelayEnvironment();
    const { getByText } = render(<TransactionList />);

    Environment.mock.rejectMostRecentOperation((operation) =>
      new Error(`A error occurred on operation: ${operation.fragment.node.name}`)
    );

    expect(getByText('Error: A error occurred on operation: TransactionListQuery')).toBeTruthy();
  });

Enter fullscreen mode Exit fullscreen mode

Resolve

To use resolveMostRecentOperation is as simple as reject.
We check if the component show loading transactions... on the screen while waiting by data and resolve a query.
Finally, we solve the query by putting our mock resolver to the generate.


it('should render success TransactionList', async () => {
    const Environment = useRelayEnvironment();
    const { getByText } = render(<TransactionList />);
    expect(getByText('loading transactions...')).toBeTruthy();

    Environment.mock.resolveMostRecentOperation(operation =>
      MockPayloadGenerator.generate(operation, {
        PageInfo() {
          return {
            hasNextPage: false,
            hasPreviousPage: false,
            startCursor: "YXJyYXljb25uZWN0aW9uOjA=",
            endCursor: "YXJyYXljb25uZWN0aW9uOjE="
          }
        },
        TransactionEdge() {
          return [
            {
              cursor: "YXJyYXljb25uZWN0aW9uOjA=",
              node: {
                id: "Q2xpZW50OjE=",
                fromUser {
                  user: "George Lima",
                  username: "georgelima",
                },
                toUser {
                  user: "Augusto Calaca",
                  username: "augustocalaca",
                },
                value: 1000,
                cashback: 50,
                message: 'A gift on your birthday'
              }
            },
            {
              cursor: "YXJyYXljb25uZWN0aW9uOjE=",
              node: {
                id: "Q2xpZW50OjI=",
                fromUser {
                  user: "Bori Silva",
                  username: "bori",
                },
                toUser {
                  user: "Augusto Calaca",
                  username: "augustocalaca",
                },
                value: 500,
                cashback: 25,
                message: 'This transaction yielded cashback'
              }
            }
          ]
        }
      })
    );

    expect(getByText('FromUser')).toBeTruthy();
    expect(getByText('Name: George Lima')).toBeTruthy();
    expect(getByText('Username: georgelima')).toBeTruthy();

    expect(getByText('ToUser')).toBeTruthy();
    expect(getByText('Name: Augusto Calaca')).toBeTruthy();
    expect(getByText('Username: augustocalaca')).toBeTruthy();

    expect(getByText('Value: 1000')).toBeTruthy();
    expect(getByText('Cashback: 50')).toBeTruthy();
    expect(getByText('Message: A gift on your birthday')).toBeTruthy();


    expect(getByText('FromUser')).toBeTruthy();
    expect(getByText('Name: Bori Silva')).toBeTruthy();
    expect(getByText('Username: bori')).toBeTruthy();

    expect(getByText('ToUser')).toBeTruthy();
    expect(getByText('Name: Augusto Calaca')).toBeTruthy();
    expect(getByText('Username: augustocalaca')).toBeTruthy();

    expect(getByText(/Value: 500/)).toBeTruthy();
    expect(getByText(/Cashback: 25/)).toBeTruthy();
    expect(getByText(/Message: This transaction yielded cashback/)).toBeTruthy(); // this is a default message
  });

Enter fullscreen mode Exit fullscreen mode

Conclusion

This is probably the baseline rule to follow when it comes to testing your relay components.
You can follow the whole the code used above on post through this link.

Oldest comments (5)

Collapse
 
arnarkari_ profile image
Arnar Kári Ágústsson

How are you able to use the useRelayEnvironment hook in your tests? Do you not get an invalid hook call error? How does the <TransactionList /> know what environment to use?

Collapse
 
simkessy profile image
simkessy

yea I get the same error, an answer would be nice.

Collapse
 
arnarkari_ profile image
Arnar Kári Ágústsson

I use createMockEnvironment from react-test-utils. Then I create a barebone component in my tests using QueryRenderer that wraps the component that I am testing, passing the fragment reference down. It's a bit of boilerplate code.

Collapse
 
simkessy profile image
simkessy

Doesn't take props?

Collapse
 
fauna5 profile image
Rich Chamberlain • Edited

You can just import the mocked environment in your tests and use it there

import environment from 'path/to/Environment';

it('should reject query with function and render error with name of the operation', () => {
    const { getByText } = render(<TransactionList />);

    environment.mock.rejectMostRecentOperation((operation) =>
      new Error(`A error occurred on operation: ${operation.fragment.node.name}`)
    );

    expect(getByText('Error: A error occurred on operation: TransactionListQuery')).toBeTruthy();
  });
Enter fullscreen mode Exit fullscreen mode