Unit Testing

React-admin relies heavily on unit tests (powered by Jest and react-testing-library) to ensure that its code is working as expected.

That means that each individual component and hook can be tested in isolation. That also means that if you have to test your own components and hooks based on react-admin, this should be straightforward.

AdminContext Wrapper

Some of react-admin’s components depend on a context for translation, theming, data fetching, etc. If you write a component that depends on a react-admin component, chances are the test runner will complain about a missing context.

Wrap your tested component inside <AdminContext> to avoid this problem:

import React from 'react';
import { AdminContext } from 'react-admin';
import { render, screen } from '@testing-library/react';

import MyComponent from './MyComponent';

test('<MyComponent>', async () => {
    render(
        <AdminContext>
            <MyComponent />
        </AdminContext>
    );
    const items = await screen.findAllByText(/Item #[0-9]: /)
    expect(items).toHaveLength(10)
})

Tip: you can also pass AdminContext as the wrapper option to the render() function:

import React from 'react';
import { AdminContext } from 'react-admin';
import { render, screen } from '@testing-library/react';

import MyComponent from './MyComponent';

test('<MyComponent>', async () => {
    render(<MyComponent />, { wrapper: AdminContext });

const items = await screen.findAllByText(/Item #[0-9]: /)
    expect(items).toHaveLength(10)
})

Mocking Providers

<AdminContext> accepts the same props as <Admin>, so you can pass a custom dataProvider, authProvider, or i18nProvider for testing purposes.

For instance, if the component to test calls the useGetOne hook:

import React from 'react';
import { AdminContext } from 'react-admin';
import { render, screen } from '@testing-library/react';

import MyComponent from './MyComponent';

test('<MyComponent>', async () => {
    render(
        <AdminContext dataProvider={{
            getOne: () => Promise.resolve({ data: { id: 1, name: 'foo' } }),
        }}>
            <MyComponent />
        </AdminContext>
    );
    const items = await screen.findAllByText(/Item #[0-9]: /)
    expect(items).toHaveLength(10)
})

Tip: If you’re using TypeScript, the compiler will complain about missing methods in the data provider above. You can remove these warnings by using the testDataProvider helper:

import React from 'react';
import { AdminContext, testDataProvider } from 'react-admin';
import { render, screen } from '@testing-library/react';

import MyComponent from './MyComponent';

test('<MyComponent>', async () => {
    render(
        <AdminContext dataProvider={testDataProvider({
            getOne: () => Promise.resolve({ data: { id: 1, name: 'foo' } }),
        })}>
            <MyComponent />
        </AdminContext>
    );
    const items = await screen.findAllByText(/Item #[0-9]: /)
    expect(items).toHaveLength(10)
})

Resetting The Store

The react-admin Store is persistent. This means that if a test modifies an item in the store, the updated value will be changed in the next test. This will cause seemingly random test failures when you use useStore() in your tests, or any feature depending on the store (e.g. datagrid row selection, sidebar state, language selection).

To isolate your unit tests, pass a new memoryStore at each test:

import { memoryStore } from 'react-admin';

test('<MyComponent>', async () => {
    const { getByText } = render(
        <AdminContext store={memoryStore()}>
            <MyComponent />
        </AdminContext>
    );
    const items = await screen.findAllByText(/Item #[0-9]: /);
    expect(items).toHaveLength(10);
})

If you don’t need <AdminContext>, you can just wrap your component with a <StoreContextProvider>:

import { StoreContextProvider, memoryStore } from 'react-admin';

test('<MyComponent>', async () => {
    const { getByText } = render(
        <StoreContextProvider value={memoryStore()}>
            <MyComponent />
        </StoreContextProvider>
    );
    const items = await screen.findAllByText(/Item #[0-9]: /);
    expect(items).toHaveLength(10);
})

Testing Permissions

As explained on the Auth Provider chapter, it’s possible to manage permissions via the authProvider in order to filter page and fields the users can see.

In order to avoid regressions and make the design explicit to your co-workers, it’s better to unit test which fields are supposed to be displayed or hidden for each permission.

Here is an example with Jest and TestingLibrary, which is testing the UserShow page of the simple example.

// UserShow.spec.js
import * as React from "react";
import { render, fireEvent } from '@testing-library/react';
import { AdminContext } from 'react-admin';

import UserShow from './UserShow';

describe('UserShow', () => {
    describe('As User', () => {
        it('should display one tab', () => {
            const testUtils = render(<UserShow permissions="user" />);

            const tabs = testUtils.queryAllByRole('tab');
            expect(tabs).toHaveLength(1);
        });

        it('should show the user identity in the first tab', () => {
            const dataProvider = {
                getOne: Promise.resolve({
                    id: 1,
                    name: 'Leila'
                })
            }
            const testUtils = render(
                <AdminContext dataProvider={dataProvider}>
                    <UserShow permissions="user" id="1" />
                </AdminContext>
            );

            expect(testUtils.queryByDisplayValue('1')).not.toBeNull();
            expect(testUtils.queryByDisplayValue('Leila')).not.toBeNull();
        });
    });

    describe('As Admin', () => {
        it('should display two tabs', () => {
            const testUtils = render(<UserShow permissions="user" />);

            const tabs = testUtils.queryAllByRole('tab');
            expect(tabs).toHaveLength(2);
        });

        it('should show the user identity in the first tab', () => {
            const dataProvider = {
                getOne: Promise.resolve({
                    id: 1,
                    name: 'Leila'
                })
            }
            const testUtils = render(
                <AdminContext dataProvider={dataProvider}>
                    <UserShow permissions="user" id="1" />
                </AdminContext>
            );

            expect(testUtils.queryByDisplayValue('1')).not.toBeNull();
            expect(testUtils.queryByDisplayValue('Leila')).not.toBeNull();
        });

        it('should show the user role in the second tab', () => {
            const dataProvider = {
                getOne: Promise.resolve({
                    id: 1,
                    name: 'Leila',
                    role: 'admin'
                })
            }
            const testUtils = render(
                <AdminContext dataProvider={dataProvider}>
                    <UserShow permissions="user" id="1" />
                </AdminContext>
            );

            fireEvent.click(testUtils.getByText('Security'));
            expect(testUtils.queryByDisplayValue('admin')).not.toBeNull();
        });
    });
});