Here at Industrial Logic, we have been working with React and React-Native extensively in the past few years. We have created a few tools that we found to accelerate our development, built through continuous refactoring of our code.

This small series will introduce these tools with a deep dive into how they work and why we think they are awesome for React and React-Native projects.

This article is about our package react-render-builder: an extensible builder interface encapsulating providers (context/data) for testing components and hooks in React-Native. It is a thin wrapper around the react-native-testing-library.

When testing a React component, we used the react-native-testing-library and its render function. Its API is straightforward. Given a React component, return an object with semantic ways of querying and interacting with the UI similar to how you would use it on the device.

import { render } from '@testing-library/react-native';

it('renders hello', () => {
    const renderApi = render(<Hello name={'Kody'} />);
    renderApi.getByText('Hello Kody!');
});

Need access to a React context/data provider? Wrap your component similar to how you wrap it in your production code.

For example, we used Redux in some of our applications, and we needed access to the Redux Provider in several component tests.

it('renders hello with Redux', () => {
    const store = createTestReduxStore({ name: 'Kody' });
    const renderApi = render(
        <Provider store={store}>
            <Hello />
        </Provider>,
    );
    renderApi.getByText('Hello Kody!');
});

As developers who use Test-Driven-Development, we write tests, lots of them. As our application started to grow, this duplication in the tests was very apparent.

At one point, we had over 30 instances of that exact Redux Provider setup! So we did the simplest thing and performed the extract method refactor to remove the duplication.

function renderWithStore(element: Element, store: Store): RenderAPI {
    return render(
        <Provider store={store}>
            {element}
        </Provider>,
    );
}

Anytime we needed to test with any new React Context/Data Provider we extended this pattern. It was very easy to compose these functions together.

function renderWithStoreAndToday(element: Element, store: Store): RenderAPI {
    return renderWithStore(
        <TodayDateTimeProvider>
            {element}
        </TodayDateTimeProvider>, 
        store,
    );
}

It was so easy we kept extending it…..

function renderWithNavigation(element: Element, store: Store) {
    return renderWithStoreAndToday(
        <NavigationContainer>
            {element}
        </NavigationContainer>, 
        store,
    );
}

As you can start to see from the example above, we started to lose context as we kept composing and creating these functions. The actual implementation of what each render function provides started to become tribal knowledge. From that tribal knowledge, some members of the team would reimplement some of the render functions in tests without realizing it.

At the same time, we started to move more of our code out of our components and into hooks for better separation of concerns. With test-driving our new hooks we ran into the same problem of needing access to React Contexts and Data Providers.

When testing a React Hook, we used the renderHook function from the same react-native-testing-library which is very similar to their render method. We started off building clones of our existing renderWith functions for our hooks.

function renderHookWithStore<T>(hook: () => T, store: Store) {
    const wrapper = (props: PropsWithChildren) => (
        <Provider store={store}>{props.children}</Provider>
    );
    return renderHook(hook, { wrapper });
}

This worked, but again continued the same smell of low-context test methods that led to confusion. Now we had two sets of functions to create and compose whenever we added a new data-provider. It was such a pain that folks would audibly exhale whenever we needed to extend our tests to support a new data provider.

Eventually, a few folks got sick of it (good developers are pretty lazy) and had a crazy idea. A common refactoring when you have multiple “with” elements is to introduce the Builder pattern from Gang Of Four. So we attempted to create RenderHookBuilder: a class that would replace each “with” function with a build method that wraps the existing elements with that given data-provider.

The implementation was a bit weird, but after a short timebox we had something working!

Client usage could now change so they can decide what data-providers to use and in what order.

it('renders hook with Builder', () => {
    const store = createTestReduxStore({ name: 'Kody' });
    const helloText = new RenderHookBuilder()
        .store(store)
        .today()
        .navigation()
        .renderHookResult(useHello);
    expect(helloText).toEqual('Hello Kody!');
});

With it being a builder as well, we were able to easily reuse and extend setups across tests.

describe('with name Kody', () => {
    let builder: RenderHookBuilder;

    beforeEach(() => {
        const store = createTestReduxStore({ name: 'Kody' });
        builder = new RenderHookBuilder().store(store);
    });

    it('shows today as current time', () => {
        const text = builder.today().renderHookResult(useTimeGreeting);
        expect(text).toEqual('Greetings Kody. The date is 2024-01-02');
    });

    it('shows yesterday as current time', () => {
        const text = builder.yesterday().renderHookResult(useTimeGreeting);
        expect(text).toEqual('Greetings Kody. The date is 2024-01-01');
    });
});

We also realized it was super easy to update all our tests to the new builder as we could change the implementation of each of our renderHookWith methods with the corresponding builder composition, and then perform an automated inline refactoring with our IDE.

Shortly after replacing all usages for our hook tests, we came up with an easy way for the builder to work with rendering React components. By supporting component rendering, we could now encapsulate our data-provider setup across all of our tests in one place. This eliminated the duplication between our component and hook tests. It protected those tests by decoupling them from specific data-provider details. Best of all, it is as simple as a two-line function to expose a data-provider for any test that requires it. Now there would be no more painful exhales when we needed to add a data-provider function for testing!

Today we have open-sourced react-render-builder, as it was an essential tool in making testing in React-Native easier in our larger applications.

To use it today, follow the steps in the react-render-builder readme. You will create a simple class to add your own builder methods to like below:

import { ReactRenderBuilder } from 'react-render-builder';

class RenderBuilder extends ReactRenderBuilder {
    store(store: Store): this {
        this.addElement(children => <Provider store={store} children={children} />);
        return this;
    }

    today(): this {
        this.addElement(children => <TodayTimeProvider children={children} />);
        return this;
    }
}

Give it a try and let us know what you think!