Automating Relay Connection Testing for a Seamless User Experience
Tags: graphql, testing, relay
Published: 2023-10-27 12:39:46

At Woovi, we have cultivated a culture of automated testing, which plays an important role in our mission to move fast and minimize regressions. Our commitment to quality testing extends to our frontend as well, where we rigorously check for various aspects on a page, from text assertions to mutation calls.

However, after refactoring a lot of code and introducing breaking changes, we started having problems with the Relay connection updates, because they were not being tested on our automatized tests. These connections are critical for ensuring a smooth user experience, particularly for real-time data updates. When these connections break, it can lead to a buggy and non-real-time platform, resulting in a poor user experience.

Testing the connection updates on Relay is not that simple since the store does not update on Relay's mock environment, the data always comes from the mock resolvers. Consequently, we needed an alternative approach to validate these connections.

# The Solution

Instead of relying on testing the Relay store, we decided to focus on asserting against Relay's generated files, which are used by Relay internally for automatizations and data handling. This approach ensures that if the query is deleted, moved, or if the structure changes, our tests will catch it.

# How it Works

We started by solving our most common use case for connections, the pagination queries. Those queries generate a separate file from which we can get the connection name. And, to get it, we use a function that will extract the connection key from the node inside the Relay generated file:

```ts
export const getQueryConnection = (request: any) => {
  const { selections } = request.operation;

  const connectionSelection = selections.find(
    (selection) =>
      selection.kind === 'LinkedHandle' && selection.handle === 'connection',
  );

  return connectionSelection.key;
};
```

With this connection name in hand, we can assert it in our tests against the mutation variables. For example:

```ts
import UserListPaginationQuery from '../__generated__/UserListPaginationQuery.graphql';

it('should test a query connection', () => {
  const userListConnectionID = ConnectionHandler.getConnectionID(
    ROOT_ID,
    getQueryConnection(UserListPaginationQuery),
  );

  expect(mutationOperation.fragment.variables).toEqual({
    input: {
      // ...
    },
    connections: [userListConnectionID],
  });
});
```

# Handling fragment connections

But if you're using connections inside a fragment, it may be nested deep within various fields, you will need to use a more complex function.

Since Relay generates the metadata according to the fragment structure, the connection name will also be nested. To extract it, we use a function that will recursively go through the fields and normalize the name for testing:

```ts
const connectionRegex = /^__([0-9a-zA-Z_]+)_connection$/;

const getSelection = (selections: any[], alias: string) => {
  for (const selection of selections) {
    if (selection.alias === alias) {
      return selection;
    }

    if (selection.name === alias) {
      return selection;
    }

    if (selection.selections) {
      const selectionFound = getSelection(selection.selections, alias);

      if (selectionFound) {
        return selectionFound;
      }
    }
  }

  return null;
};

export const getFragmentConnection = (request: any, field: string) => {
  const aliases = field.split('.');
  
  const connection = aliases.reduce(
    (selection, alias) => getSelection(selection.selections, alias),
    request,
  );

  const [, name] = connectionRegex.exec(connection.name) || [];

  return name;
};
```

With `getFragmentConnection`, you can test connections within fragments, even when they are deeply nested by passing its path as the second argument:

```ts
import Banner_user from '../__generated__/Banner_user.graphql';

it('should test a fragment connection', () => {
  const bannerConnectionID = ConnectionHandler.getConnectionID(
    company.id,
    getFragmentConnection(Banner_user, 'company.accounts'),
  );
  
  expect(mutationOperation.fragment.variables).toEqual({
    input: {
      // ...
    },
    connections: [bannerConnectionID],
  });
});
```

# Drawbacks and Considerations

While this testing approach offers a robust solution, there are some challenges. Since it relies on Relay's generated files, locating the correct file for testing does not offer a good DX:
- IDEs may often be configured to ignore these files, making them challenging to find because of the lack of autocomplete.
- These files can be distant from the current test file, complicating the process of finding the generated file path.

Also, this solution does not test the connection update type, so if you change the update from `appendNode` to `prependNode`, it will still pass.

In conclusion, by automating the testing of Relay connections, we have significantly improved the reliability and stability of our user experience. While there are challenges, the benefits of preventing broken connections and ensuring a seamless user experience make this approach highly valuable in our development process.

---
Woovi

[Woovi](https://www.woovi.com) is a Startup that enables shoppers to pay as they like. To make this possible, Woovi provides instant payment solutions for merchants to accept orders.

If you want to work with us, we are [hiring](https://woovi.com/jobs/)!

---
Photo by <a href="https://unsplash.com/@hansreniers?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Hans Reniers</a> on <a href="https://unsplash.com/photos/three-clear-beakers-placed-on-tabletop-lQGJCMY5qcM?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>