Get better NOW at unit tests with React Testing Library

There are a lot of examples out there for writing tests with react testing library that I wish did not exist because they meant spending years with ineffective tests. Not considering how you write your tests can lead to flaky tests, hard-to-maintain tests, and tests that don’t do anything or what you think they are doing.

Ever seen this?

import {getAccount} from 'account-api';
import AccountPage from 'AccountPage';
jest.mock('account-api');
test('Account render', () => {
	const promise = Promise.resolve(getAccountTestData());
	getAccount.mockReturnValue(promise);
	
	const { getByText, queryAllByText } = render(
 <AccountPage />
);
	return promise.then( () => {
		 expect(getAccount).toHaveBeenCalledTimes(1);
		 expect(getByText('Account Summary')).toBeInTheDocument();
		 expect(getByText('Update Password')).toBeInTheDocument();
		 // Plus other expects
	});
});

When reading the internet on how to write react testing library and jest tests, this seems to be the standard. This however has flaws that at first may just seem minor, but can actually make your tests flaky, high maintenance, and may not actually test anything.

When I first read a test like that, I assumed that the promise.then was actually running after the promise runs on the AccountPage as a continuation of the promise chain. This is not the case. To really understand this you need to know that when you call promise.then, it starts the promise, if you call it again, it starts it again, it is not the same promise.

When using return promise.then( you are just writing a timing hack. This only works because it has initialized AccountPage before the return promise.then. What will the below print out?

test('Test', () => {
    const p = Promise.resolve('test');
    p.then((res) => {
       console.log(res)
    })
    p.then((res) => {
       console.log(res)
    })
})

If you said “test” twice, you are right.

So what is the big deal you ask? How does this make your tests flaky, high maintenance, and may not actually test anything? let’s go over some scenarios.

Adding another API call.

Now the account page has another API call on it, let’s just pretend it is fetching more account data. Now, the test fails, but for the wrong reason, purely because of a timing reason. So now you might be tempted to write something like:

import {getAccount, getExtraAccountDetails} from 'account-api';
import AccountPage from 'AccountPage';
jest.mock('account-api');
test('Account render', () => {
	const promise = Promise.resolve(getAccountTestData());
	getAccount.mockReturnValue(promise);
	
	const promise2 = Promise.resolve(getExtraAccountTestData());
	getExtraAccountDetails.mockReturnValue(promise);
	
	const { getByText, queryAllByText } = render(
			<AccountPage />
		);
	return promise.then( () => {
	    return promise2.then( () => {
			expect(getAccount).toHaveBeenCalledTimes(1);
			expect(getByText('Account Summary')).toBeInTheDocument();
			expect(getByText('Update Password')).toBeInTheDocument();
			// Plus other expects
		});
	});
});

That will work, you will get a passing test. Hurray! But should you be happy with it? I think no. It is already starting to get messy and confusing with the promises and these are just two (I have seen this done with many more). Read on for another problem.

Testing negative scenarios

Now you want to test the negative scenario. Let’s just say, the first promise is now rejecting instead-but woops. you did not change promise.then to a promise.catch. Your assertions do not get executed or verified, in other words, your test is doing nothing. This is where someone with experience writing these kinds of these chimes in and says, “ah but jest has expect.assertions”. Oh good problem solved. It does solve that by making sure it is not being tested in the first place problem described above, but it is a bandaid solution to a problem that could easily be solved by writing tests in other ways. It also does not stop your tests from getting out of hand with promise chaining.

Here is what I think is the best way to write unit tests with react testing library and jest. Check out the comments for details:

import {getAccount, getExtraAccountDetails} from 'account-api';
import AccountPage from 'AccountPage';
jest.mock('account-api');
test('Account render', async () => {
	// You do not need to assign promises to anything. Not necessary. Never was.
	getAccount.mockReturnValue(Promise.resolve(getAccountTestData()));
	getExtraAccountDetails.mockReturnValue(Promise.resolve(getExtraAccountTestData()));
	
	const { getByText, waitFor, findByText } = render(<AccountPage />);
        // Wait for the promises you want to happen before checking anything else
	await waitFor(() => {
		expect(getAccount).toHaveBeenCalledTimes(1);
		expect(getExtraAccountDetails).toHaveBeenCalledTimes(1);
	});
	
	// Use await findByText when waiting for something that comes from an API. 
	// As the render function may not have completed when you goto check.
	// Use findByText if you are having problems on a slower machine or CI
	expect(await findByText('myemail@email.com').toBeInTheDocument();
	
	// Verifying headers and static values. I just like to do this last. 
	expect(getByText('Account Summary')).toBeInTheDocument();
	expect(getByText('Update Password')).toBeInTheDocument();
});

I hope you see the above test looks cleaner, if there is a failure then you will hopefully be able to trace its cause faster. Also if you did change to reject instead of resolve, the failure will come from the value assertions on your page, not an error about the value in expect.assertions() being valid.

Similar Posts