DEV Community

Cover image for React Testing Library custom Render with options for Providers.
Rolando Niubo
Rolando Niubo

Posted on • Updated on

React Testing Library custom Render with options for Providers.

There is multiple ways to setup a Custom Render that has already all your providers. Here you are going to see some sintax honey that will allows you to render your components using the React Testing Library, passing as options the providers you want to activate around that Component.

In this similar way:

renderUi(<Component />, {
    withReduxProvider: true,
    withReactQueryProvider: true,
})
Enter fullscreen mode Exit fullscreen mode

In this example we use some Providers that you might not use on your application, also you might use some other I'm not using here. Just addapt your code following this same pattern. The same goes for versions, this code might change on the basis of how this Libraries change their implementation.

Let's continue.

First create our helper file in where we are going to make this possible:

Implementation

// this file simplifies the component setup process when rendering the component for testing

import React from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { Provider } from 'react-redux';
import { MemoryRouter as Router } from 'react-router-dom';
import { QueryClientProvider } from 'react-query';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import axios from 'axios';
import store from '/services/rootReducer';
import { createTestQueryClient } from '/services/test/utils';

interface IExtendedRenderOptions extends RenderOptions {
  withRouter?: boolean
  routerHistory?: string[]
  withRedux?: boolean
  mockInitialState?: any
  withQueryProvider?: boolean
  mockAxiosCalls?: any
}

// wrappers and contexts
const wrapInRedux = (componentTree: JSX.Element, { mockInitialState }: IExtendedRenderOptions) => {
    const storeConfig = configureStore([thunk]);
    const storeMock = mockInitialState ? storeConfig(mockInitialState) : store;
    // This allows you to use a passed mock store
    return (
        <Provider store={storeMock}>
            {componentTree}
        </Provider>
    );
};

const wrapInRouter = (componentTree: JSX.Element, routerHistory?: string[]) => (
    <Router initialEntries={routerHistory}>
        {componentTree}
    </Router>
);

const wrapInQueryProvider = (componentTree: JSX.Element) => {
    const testQueryClient = createTestQueryClient();
    return (
        <QueryClientProvider client={testQueryClient}>
            {componentTree}
        </QueryClientProvider>
    );
};


// You don't need this option if you are using something more advance such as Mock Service Worker
// This Function Mock axios calls and hand back a response for each call finding the right response by comparing the called endpoint with the endpoint fragment you passed
// You need to pass an array of objects, each object will represent a call
// { endpointFragment: string, expectedMockResponse: yourMockType }
type AxiosCallMock = { endpointFragment: string, expectedMockResponse: any }
// This one will runs on each axios get request
const mockAxiosCallResponsesIfAny = (renderOptions?: IExtendedRenderOptions) => {
    if (renderOptions?.mockAxiosCalls) {
        axios.get.mockImplementation((url) => {
            // If your component has multiple axios calls make sure to pass
            // endpoint fragments that identify them
            const optionObject = renderOptions?.mockAxiosCalls.find((mock: AxiosCallMock) => url.includes(mock.endpointFragment));
            return Promise.resolve(optionObject.expectedMockResponse);
        });
    }
};

const setupComponent = (ui: JSX.Element, renderOptions?: IExtendedRenderOptions) => {
    if (!renderOptions) return ui;
    let componentTree = <>{ui}</>;
    if (renderOptions.withRouter) componentTree = wrapInRouter(componentTree, renderOptions.routerHistory);
    if (renderOptions.withRedux) componentTree = wrapInRedux(componentTree, renderOptions);
    if (renderOptions.withQueryProvider) componentTree = wrapInQueryProvider(componentTree);
    return componentTree;
};

const customRender = (ui: JSX.Element, renderOptions?: IExtendedRenderOptions) => {
    try {
        mockAxiosCallResponsesIfAny(renderOptions);
        const componentTree = setupComponent(ui, renderOptions);
        return render(componentTree);
    } catch (error) {
        console.log(error);
        throw error;
    }
};

export * from '@testing-library/react';
export { customRender, IExtendedRenderOptions };

Enter fullscreen mode Exit fullscreen mode

Usage

This is how we render the component in our Test

// Component.test.tsx
import React from 'react';
// we import out customRender
import { customRender, screen, fireEvent, waitFor } from 'ns_libs/testing/testUtils';
import Component from 'pages';

const initialProps: IProps = {
    toggle: () => {},
    isOpen: true,
    target: 'test-popover-target',
    feature: FEATURES_LIST.PORTFOLIO_GROUPS,
    subprojectId: 0,
};

// initialize redux state with any value you want
// maybe same values that is initialized in your app
const initialReduxState = {
           user: {
              name: "",
              age: 0,
           }
        }

const renderUi = (props: IProps = initialProps) => customRender(
    <Component {...props} />, {
        withRouter: true,
        routerHistory: [':some_path_maybe'],
        withRedux: true,
        mockInitialState: initialReduxState,
    },
);
Enter fullscreen mode Exit fullscreen mode

This is an example of passing mock data on mock axios calls, if you still need to use that option to mock axios calls happening in your component:

import React from 'react';
import { customRender, screen, act, waitFor } from 'ns_libs/testing/testUtils';
import '@testing-library/jest-dom';
import axios from 'axios';

//  Sample Component
const SampleComponent = () => {
    const [data, setData] = React.useState<any>();
    const [dataTwo, setDataTwo] = React.useState<any>();

    const getTodos = async (): Promise<any> => {
        try {
            const res = await axios.get('https://yourendpoint.todos.com');
            setData(res);
        } catch (error) {
            console.log(error);
        }
    };

    const getOtherThings = async (): Promise<any> => {
        try {
            const res = await axios.get('https://endpoint.otherthings.com');
            setDataTwo(res);
        } catch (error) {
            console.log(error);
        }
    };

    React.useEffect(() => {
        getTodos();
        getOtherThings();
    }, []);

    return (
        <div>
            {data && <h1>{data?.map((item: any) => <span key={item.email}>{item.title}</span>)}</h1>}
            {dataTwo && <h1>{dataTwo?.map((item: any) => <span key={item.email}>{item.title}</span>)}</h1>}
        </div>
    );
};

export default SampleComponent;

// Here starts the test for Sample Component

const ResponseMock: any = [
    {
        endpointFragment: '/todos',
        expectedMockResponse: [
            {
                title: 'Nice First Title',
                description: 'The todo is this',
            },
                title: 'Nice Second Title',
                description: 'The todo is this',
            {
            },
        ],
    },
    {
        endpointFragment: '/otherthings',
        expectedMockResponse: [
            {
                title: 'First Other',
                content: 0,
            },
            {
                title: 'Second Other',
                content: 0,
            },
        ],
    },
];

const renderUi = () => customRender(<SampleComponent />, {
    mockAxiosCalls: ResponseMock,
});

describe('Test Component', () => {
    afterEach(() => {
        jest.resetAllMocks();
    });

    test('First test', async () => {
        renderUi();
        await waitFor(() => {
            const title = screen.getByText('Nice First Title');
            const title2 = screen.getByText('Nice Second Title');
            const title3 = screen.getByText('First Other');
            const title4 = screen.getByText('Second Other');
            expect(title).toBeInTheDocument();
            expect(title2).toBeInTheDocument();
            expect(title3).toBeInTheDocument();
            expect(title4).toBeInTheDocument();
        });
    });
});

Enter fullscreen mode Exit fullscreen mode

Discussion (0)