Home > front end >  Make Unit Test Wait for Data filled by Asynchronous Fetch
Make Unit Test Wait for Data filled by Asynchronous Fetch

Time:09-17

I have a unit test which includes rendering a component that uses useSWR to fetch data. But the data is not ready before the expect() is being called so the test fails.

test("StyleOptionDetails contains correct style option number", () => {
    renderWithMockSwr(
        <StyleOptionDetails
            {...mockIStyleOptionDetailsProps}
        />
    )
    expect(screen.getByText(123)).toBeInTheDocument();
});

But if I put in a delay setTimeout(), it will pass the test.

setTimeout(() => {
    console.log('This will run after 2 second')
    expect(screen.getByText(123)).toBeInTheDocument();
}, 2000);

What is the correct way to create a delay or wait for the data?

CodePudding user response:

I have a similar answer elsewhere for a ReactJS question, Web Fetch API (waiting the fetch to complete and then executed the next instruction). Let me thresh out my solution for your problem.

If your function renderWithMockSwr() is asynchronous, then if you want it to wait to finish executing before calling the next line, use the await command.

await renderWithMockSwr(
    <StyleOptionDetails
        {...mockIStyleOptionDetailsProps}
    />
)

async is wonderful. So is await. Check it out: Mozilla Developer Network: Async Function

CodePudding user response:

Although I think you are already doing this, the first thing to note is that you shouldn't be actually fetching any data from your tests-- you should be mocking the result.

Once you are doing that, you will use the waitFor utility to aid in your async testing-- this utility basically accepts a function that returns an expectation (expect), and will hold at that point of the test until the expectation is met.

Let's provide an example. Take the imaginary component I've created below:

const MyComponent = () => {
    const [data, setData] = useState();
    useEffect(() => {
        MyService.fetchData().then((response) => setData(response));
    }, []);

    if (!data) {
        return (<p>Loading...</p>);
    }
    // else
    return (
        <div>
            <h1>DATA!</h1>
            <div>
                {data.map((datum) => (<p>{datum}</p>))}
            </div>
        </div>
    );
}

So for your test, you would do

import MyService from './MyService';
import MyComponent from './MyComponent';

describe('MyComponent', () => {
    const mockData = ['Spengler', 'Stanz', 'Venkman', 'Zeddmore'];
    beforeEach(() => {
        jest.spyOn(MyService, 'fetchData')
            .mockImplementation(
                () => new Promise((res) => setTimeout(() => res(mockData), 200))
            );
    });
    afterEach(() => {
        MyService.fetchData.mockRestore();
    });
    it('displays the loading first and the data once fetched', async () => {
        render(<MyComponent />);
        // before fetch completes
        expect(screen.getByText(/Loading/)).toBeInTheDocument();
        expect(screen.queryByText('DATA!')).toBeNull();
        // after fetch completes..
        // await waitFor will wait here for this to be true; if it doesn't happen after about five seconds the test fails
        await waitFor(() => expect(screen.getByText('DATA!')).toBeInTheDocument());
        expect(screen.queryByText(/Loading/)).toBeNull(); // we don't have to await this one because we awaited the proper condition in the previous line
    });
});

This isn't tested but something like this should work. Your approach to mocking may vary a bit on account of however you've structured your fetch.

  • Related