Make Unit Test Wait for Data filled by Asynchronous Fetch
Asked Answered
S

2

10

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?

Sapota answered 10/9, 2021 at 13:25 Comment(5)
Hey, this answer I have might help you, Web Fetch API (waiting the fetch to complete and then executed the next instruction). Good luck.Sea
use findBy or waitFor testing-library.com/docs/dom-testing-library/api-asyncHinton
You can also make the test case 'async' and use 'await' before your asynchronous statement.Tarnetgaronne
In all likelihood that expectation isn't reached during the test - the test passing doesn't tell you anything useful. Read jestjs.io/docs/asynchronous.Ferrigno
Thank you all for the input! Making the test async and using await findBy instead of getBy did the job. If you feel like answering the question then I'll checkmark it as answer - I guess whoever is first @Sea @Hinton @TarnetgaronneSapota
S
1

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

Sea answered 13/9, 2021 at 14:40 Comment(0)
H
3

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.

Hammered answered 13/9, 2021 at 15:11 Comment(4)
what if the result is the mock data coming from my express server ? Meaning that I fetch the data from my server and rendering that mock data within my component that consumes mock data.Coatbridge
@Coatbridge Personally, I would avoid something like this. As far as I understand the guiding principles of unit testing, you want to minimize any potential variables, with a goal towards isolating the unit you are testing as much as possible to ensure you are testing the unit and the unit alone, without any complication arising from external conditions. Furthermore, it keeps the testing data and the test in the same place, for ease of readability in maintenance. I'm not suggesting that there is no possible scenario under which your setup is valid, but I'm not sure what it would be.Hammered
I decided to use cypress for abstracting away the limitations of this situation. I guess it is better suited for integration/e2e. And I still get to 'unit test' my form (login) for instance. Do you think I'm deviating from unit testing ? I'd seen others use cypress to apply unit tests as well.Coatbridge
@Coatbridge - to be honest, I've not used Cypress, but my understanding is the same as yours-- that essentially it is more of an integration/e2e test. Which, to be clear, is also important-- integration/e2e testing is another tool in the toolbelt in making sure that functioning, bug-free code is deployed to production. In my experience it is usually a combo of unit testing, integration/e2e testing, and human testing / smoke testing that is applied to try to maintain code quality. You'll need to find your own balance, but that you are testing at all is a good start :)Hammered
S
1

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

Sea answered 13/9, 2021 at 14:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.