TLDR: I broke some tests and don't know why.
I needed to put some existing code behind a asynchronous conditional check. My existing tests asserted that a mock function that was getting passed via props was get called, but now this function was behind the async code/condition. I updated the tests with a spy for the condition check so the execution could continue as normal and it is, however the mock function is failing the assertion that it was called. To further add to the confusion, when I step through the tests & code, the expected mock functions exist and are seemingly called. Below is an approximation of the before & after of my code with comments pointing out which assertions pass/fail. In my actual codebase, I added the condition in multiple places where those components are being tested with other frameworks not included in the approximation including enzyme/mount & react-test-renderer but still getting the failing tests for the same reasons.
// SearchButton.js
export const SearchButton = ({ searchFunction }) => {
const handleSubmit = () => {
searchFunction();
};
return (
<button
id="search-button-submit"
data-testid="search-button-submit"
onClick={() => handleSubmit()}
/>
);
};
// SearchButton.test.js
import "@testing-library/jest-dom";
import { render } from "@testing-library/react";
import { SearchButton } from "./SearchButton.js";
describe("SearchButtonTest", () => {
it("should call search function on submit", () => {
const searchFunctionMock = jest.fn();
const { getByTestId } = render(
<SearchButton searchFunction={searchFunctionMock} />
);
fireEvent.click(getByTestId("search-icon-submit"));
expect(searchFunctionMock).toHaveBeenCalled(); // this passes
});
});
// Updated SearchButton.js
import { isUserLoggedIn, openLoginModal } from "./utils.js";
export const SearchButton = ({ searchFunction }) => {
const handleSubmit = async () => {
try {
const loggedIn = await isUserLoggedIn();
if (loggedIn) {
searchFunction();
} else {
openLoginModal();
}
} catch (error) {
console.log(error);
}
};
return (
<button
id="search-button-submit"
data-testid="search-button-submit"
onClick={() => handleSubmit()}
/>
);
};
// Updated SearchButton.test.js
import "@testing-library/jest-dom";
import { render } from "@testing-library/react";
import { SearchButton } from "./SearchButton.js";
import * as utils from "./utils.js";
describe("SearchButtonTest", () => {
it("should call search function on submit", () => {
const searchFunctionMock = jest.fn();
const isUserLoggedInSpy = jest
.spyOn(utils, "isUserLoggedIn")
.mockResolvedValue(true);
const { getByTestId } = render(
<SearchButton searchFunction={searchFunctionMock} />
);
fireEvent.click(getByTestId("search-icon-submit"));
expect(isUserLoggedInSpy).toHaveBeenCalled(); // passes
expect(searchFunctionMock).toHaveBeenCalled(); // this no longer passes
});
});
CodePudding user response:
You should use waitFor to wait for the promise of the mock async function isUserLoggedIn
to resolve.
waitFor
may run the callback a number of times until the timeout is reached. Note that the number of calls is constrained by the timeout and interval options.
This can be useful if you have a unit test that mocks API calls and you need to wait for your mock promises to all resolve.
E.g.
SearchButton.tsx
:
import React from 'react';
import { isUserLoggedIn, openLoginModal } from './utils';
export const SearchButton = ({ searchFunction }) => {
const handleSubmit = async () => {
try {
const loggedIn = await isUserLoggedIn();
if (loggedIn) {
searchFunction();
} else {
openLoginModal();
}
} catch (error) {
console.log(error);
}
};
return <button id="search-button-submit" data-testid="search-button-submit" onClick={() => handleSubmit()} />;
};
utils.ts
:
export const isUserLoggedIn = async () => false;
export const openLoginModal = () => {};
SearchButton.test.tsx
:
import { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';
import { SearchButton } from './SearchButton';
import * as utils from './utils';
describe('SearchButtonTest', () => {
it('should call search function on submit', async () => {
const searchFunctionMock = jest.fn();
const isUserLoggedInSpy = jest.spyOn(utils, 'isUserLoggedIn').mockResolvedValue(true);
const { getByTestId } = render(<SearchButton searchFunction={searchFunctionMock} />);
fireEvent.click(getByTestId('search-button-submit'));
expect(isUserLoggedInSpy).toHaveBeenCalled();
await waitFor(() => expect(searchFunctionMock).toHaveBeenCalled());
});
});
Test result:
PASS stackoverflow/73240358/SearchButton.test.tsx (11.017 s)
SearchButtonTest
✓ should call search function on submit (79 ms)
------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files | 80 | 50 | 57.14 | 84.62 |
SearchButton.tsx | 85.71 | 50 | 100 | 81.82 | 12-15
utils.ts | 66.67 | 100 | 0 | 100 |
------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 11.63 s