As a side project, I’m currently working on a Chrome extension built using Create React App, Typescript, and the Redux Toolkit. For testing, I’m using Jest (set up for me by Create React App) and React Testing Library. Recently, I ran into a weird problem with my test suite: an assertion that a certain function was being called was failing, even though I knew for a fact that it was, indeed, being called. Here’s the code:
const proceduresAsAny = (procedures as any)
test("Add bookmark inside works", async () => {
const createBookmarkNode = procedures.createBookmarkNode,
createBookmarkNodeMock = jest.fn()
proceduresAsAny.createBookmarkNode = createBookmarkNodeMock
user.click(screen.getByTestId("folder-menu-add-bookmark-inside"))
expect(createBookmarkNodeMock).toHaveBeenCalledTimes(1)
const expectedArgs: chrome.bookmarks.BookmarkCreateArg = {
parentId: "2.1",
title: "New Bookmark",
url: "https://google.com"
}
expect(createBookmarkNodeMock).toHaveBeenCalledWith(
expect.anything(),
expectedArgs
)
proceduresAsAny.createBookmarkNode = createBookmarkNode
})
How did I know the mock function was actually being called? Simple, by giving it an implementation that would log to the console:
createBookmarkNodeMock = jest.fn(
() => console.log("calling createBookmarkNodeMock!")
)
Here the Jest console was indeed printing "calling createBookmarkNodeMock!". So what gives? Why was the assertion failing?
Turns out the issue was async-related. Indeed, the button’s click event handler wasn’t just changing a component’s state or dispatching a simple Redux event, it was dispatching an async thunk that contained an await, meaning that everything that followed the await in the body of the thunk would be run after my test function finished running. Not too useful when I’m trying to test the effects of that button click!
So what’s the solution? All the advice I was finding online was saying the same thing: mock the store! Even the official Redux docs were saying that (see https://redux.js.org/recipes/writing-tests#async-action-creators). Now that’s cool and all, but there’s just one problem with that: I don’t want to mock the store! I’m trying to write a test that shows that my function is being called in a specific way when a certain button is pressed. I don’t care if that’s being done by sending a certain event to the store. In fact, I don’t even care if Redux is used at all! As long as my function ends up being called, I’m happy. So how can I write such a test?
The solution I found was pretty simple: React Testing Library’s waitFor function. waitFor will repeatedly call the given function until it doesn’t throw an error. So all I needed to do to get my test working was to replace this line:
expect(createBookmarkNodeMock).toHaveBeenCalledTimes(1)
With this:
await waitFor(() =>
expect(createBookmarkNodeMock).toHaveBeenCalledTimes(1)
)
Is there a better way of accomplishing this without using waitFor? Do you disagree and think I should actually mock the store? Let me know in the comments!
Bonus: why is my mock OK?
In not wanting to mock the Redux store here, I’m certainly not saying that all mocks are bad. In the test shown earlier, for instance, I’m mocking a function that calls the Chrome API to create a bookmark node. I don’t want to actually call this function in my tests because 1) Setting up an environment where I can call this real API is complicated, 2) I’m very confident that the feature I’m testing is implemented correctly if the system actually calls the mocked function in the way that the test describes, and 3) I expect a test using the real Chrome API to be slower than the one using a mock. For these reasons, I’m happy to mock out the createBookmarkNode function (and in fact, I created this function specifically because I wanted to mock it out).
Cover photo by Wei Pan on Unsplash
Top comments (0)