Where should I use act when testing an async react hook? - reactjs

When testing an async react hook with #testing-library/react-hooks I see an error message. The error message mentions wrapping code in act(...) but I'm not sure where I should do this.
I have tried to wrap parts of the code in act(...) but each attempt leads to the test failing.
// day.js
import { useState, useEffect } from 'react';
import { getDay } from '../api/day';
export function useDay() {
const [state, set] = useState({ loading: false });
useEffect(() => {
let canSet = true;
set({ loading: true });
const setDay = async () => {
const day = await getDay();
if (canSet) {
set(day);
}
};
setDay();
return () => (canSet = false);
}, []);
return state;
}
// day.test.js
import { renderHook, act } from "#testing-library/react-hooks";
import { useDay } from "./day";
jest.mock("../api/day", () => ({
getDay: jest.fn().mockReturnValue({ some: "value" })
}));
describe.only("model/day", () => {
it("returns data", async () => {
const { result, waitForNextUpdate } = renderHook(() => useDay());
await waitForNextUpdate();
expect(result.current).toEqual({ some: "value" });
});
});
// test output
console.error node_modules/react-test-renderer/cjs/react-test-renderer.development.js:102
Warning: An update to TestHook inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */

This is a known issue: https://github.com/testing-library/react-testing-library/issues/281
Before 16.9.0-alpha.0 React itself didn't handle the async stuff pretty good, so that has nothing to do with the testing library, really. Read the comments of the issue if you're interested in that.
You have two options now:
Update your React (& react-dom) to 16.9.0-alpha.0
Add a snippet (e. g. in your test setup file) to suppress that warning when console.log tries to print it:
// FIXME Remove when we upgrade to React >= 16.9
const originalConsoleError = console.error;
console.error = (...args) => {
if (/Warning.*not wrapped in act/.test(args[0])) {
return;
}
originalConsoleError(...args);
};

Related

Testing a custom React hook that shows an exit prompt

I apologize if this question has been asked before. I did some searching but couldn't find an answer. I'm fairly new to React, so I may be missing something obvious.
I have inherited some TypeScript React code that includes a custom React hook to display a prompt when a user reloads or exits a page when there are edits on the page.
import { useState, useEffect } from 'react';
const initBeforeUnload = (showExitPrompt: boolean) => {
window.onbeforeunload = (event: BeforeUnloadEvent) => {
if (showExitPrompt) {
const e = event || window.event;
e.preventDefault();
if (e) {
e.returnValue = '';
}
return '';
}
};
};
// Hook
const useExitPrompt = (
bool: boolean
): [boolean, (showPrompt: boolean) => void] => {
const [showExitPrompt, setShowExitPrompt] = useState<boolean>(bool);
window.onload = () => {
initBeforeUnload(showExitPrompt);
};
useEffect(() => {
initBeforeUnload(showExitPrompt);
}, [showExitPrompt]);
useEffect(() => () => {
initBeforeUnload(false);
}, []);
return [showExitPrompt, setShowExitPrompt];
};
export default useExitPrompt;
No unit tests were included with this code when I inherited it, so I'm writing tests now. So far I've come up with the following:
/**
* #jest-environment jsdom
*/
import { renderHook, act } from '#testing-library/react-hooks';
import useExitPrompt from './useExitPrompt';
describe('useExitPrompt', () => {
it('should update the value of showExitPrompt', () => {
const { result } = renderHook(({ usePrompt }) => useExitPrompt(usePrompt),
{
initialProps: {
usePrompt: true
}
});
const [initialShowExitPrompt, setShowExitPrompt] = result.current;
expect(initialShowExitPrompt).toBe(true);
act(() => {
setShowExitPrompt(false);
});
const [updatedShowExitPrompt] = result.current;
expect(updatedShowExitPrompt).toBe(false);
});
});
This test passes, but it doesn't really get to the meat of what the hook is about: when showExitPrompt is true and the page is unloaded, is the exit prompt displayed? (In fact, it really only tests that the useState hook works, which is not what I want at all.) Unfortunately, I've run into a brick wall in trying to figure out how to test this; any suggestions?

WaitForNextUpdate of renderHook of react-testing-library timeout

I'm testing a custom hook with react-testing-library which basically does this:
function useHook() {
const [state, setState] = useState();
const fetch = async () => {
const response = await httpCall();
if (instanceof response !== Error) {
setState("GOOD")
} else {
setState("BAD")
}
}
return { state, fetch }
}
and my test file is something like this:
it("test", async () => {
const { result, waitForNextUpdate } = renderHooks(() => useHook())
await result.current.fetch();
expect(result.current.state).toBe(undefined)
await waitForNextUpdate();
expect(result.current.state).toBe("GOOD") //or at least "BAD"
})
I wrote this because I called the async function fetch() that should trigger the setState, I assert that no rerender has been occurred yet, and then I waitForNextUpdate() in order to wait this rerender and I assert that the state returned by the hooks has now a value "GOOD" or "BAD".
My problem is that my test gives me an error: Timeout - Async callback was not invoked within the 5000 ms ..., and this error occurred when the test waits for the waitForNextUpdate().
I don't know what's wrong with my test. I'm sure (because i tested it) that the hook is working properly, the http call has been made. I know that checking values inside the test but also because the hook is working properly inside the application.
I don't understand why it seems that the update of the state never occures.
I'm the first one of my team who is testing with this tool so i'm quite lost.
First of all you have small mistake in if statment of the hook, so let's correct that and also add import of httpCall function for example sake
import { useState } from 'react'
import { httpCall } from './httpCall'
export function useHook() {
const [state, setState] = useState<string>()
const fetch = async () => {
const response = await httpCall()
if (response instanceof Error) {
setState('GOOD')
} else {
setState('BAD')
}
}
return { state, fetch }
}
now we can have two test cases, based on mocking httpCall results. Note that rerender is used instead of waitForNextUpdate
import { waitFor } from '#testing-library/react'
import { renderHook } from '#testing-library/react-hooks'
import { httpCall } from './httpCall'
import { useHook } from './useHook'
jest.mock('./httpCall', () => ({
httpCall: jest.fn()
}))
describe('useHook', () => {
it('should return error', async () => {
// httpCall was mocked above, so we can replace it's implementation
httpCall.mockImplementation(() => new Error('error'))
const { result, rerender } = renderHook(() => useHook())
await result.current.fetch()
rerender()
await waitFor(() => {
expect(result.current.state).toBe('BAD')
})
})
it('should return response', async () => {
httpCall.mockImplementation(() => ({
ok: true
}))
const { result, rerender } = renderHook(() => useHook())
await result.current.fetch()
rerender()
await waitFor(() => {
expect(result.current.state).toBe('GOOD')
})
})
})
You should not use await when calling the function under test of your hook.
It should instead be wrapped in act(), though as specified in the documentation about async testing it can be omitted when await waitForNextUpdate() is called.
The test passes with this change :
it('test', async () => {
const { result, waitForNextUpdate } = renderHook(() => useHook());
act(() => {
result.current.fetch();
});
expect(result.current.state).toBe(undefined);
await waitForNextUpdate();
expect(result.current.state).toBe('GOOD'); //or at least "BAD"
});

unit test custom hook with jest and react testing library

I am trying to unit test a custom hook using jest and react testing library in scenario where error is thrown but I am not able to catch the actual error message, here is my code so far:
my first hook:
import react from 'react';
const useFirstHook = () => {
//I will add conditional logic later
throw new Error('my custom error is thrown')
const test1 = 'I am test 1';
return {
test1
};
};
export default useFirstHook;
test.js
import React from 'react';
import { render } from '#testing-library/react';
import useFirstHook from './useFirstHook';
describe('useFirstHook', () => {
//I also tried adding jest.spy but no luck
/* beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {})
}); */
it('test 1', () => {
let result;
const TestComponent = () => {
result = useFirstHook()
return null;
};
render(<TestComponent />)
//expect()
});
});
my logic is to first create a hook, unit test it and then create component, add hook there and test that component with hook integration as well. what am I missing, or my approach is completely wrong ?
A good approach would be testing the component itself, that already contains the hook.
In case you consider that the hook needs to be test without the component, you can use #testing-library/react-hooks package, something like:
const useFirstHook = (shouldThrow = false) => {
// throw onmount
useEffect(() => {
if (shouldThrow) throw new Error('my custom error is thrown');
}, [shouldThrow]);
return {
test1: 'I am test 1'
};
};
describe('useFirstHook', () => {
it('should not throw', () => {
const { result } = renderHook(() => useFirstHook(false));
expect(result.current.test1).toEqual('I am test 1');
});
it('should throw', () => {
try {
const { result } = renderHook(() => useFirstHook(true));
expect(result.current).toBe(undefined);
} catch (err) {
expect(err).toEqual(Error('my custom error is thrown'));
}
});
});

Why do I see the warning about using act when testing this hook?

I'm having trouble understanding how to write a test for a hook without the following warning when using renderHook from "#testing-library/react-hooks".
"Warning: An update to TestHook inside a test was not wrapped in act(...)."
Basically the hook sets initial value in state using useState and then within a useEffect hook I do something asynchronously which ends up updating the state value.
import React from "react";
// fake request
const fetchData = () => Promise.resolve("data");
export const useGetData = () => {
const initialData = { state: "loading" };
const [data, setData] = React.useState(initialData);
React.useEffect(() => {
fetchData()
.then(() => setData({ state: "loaded" }));
}, []);
return data;
};
The hook simply returns the state value at all times.. so I've written a test to assert that it returns the initial value at first and eventually returns the new state value.
import { renderHook } from "#testing-library/react-hooks";
import { useGetData } from "./useGetData";
describe("useGetData", async () => {
it('Should initially return an object with state as "loading"', () => {
const { result } = renderHook(() => useGetData());
expect(result.current).toEqual({ state: "loading" });
});
it('Should eventually return an object with state as "loaded"', async () => {
const { result, waitForNextUpdate } = renderHook(() => useGetData());
await waitForNextUpdate();
expect(result.current).toEqual({ state: "loaded" });
});
});
I've created a sandbox that replicates this:
https://codesandbox.io/s/dazzling-faraday-ht4cd?file=/src/useGetData.test.ts
I've looked into what this warning means and what act is.. but for this particular scenario I'm not sure whats missing.
You can fix it by doing this:
await act(async () => {
await waitForNextUpdate();
});
You need to wrap any function that's going to update the state by the act function

error when creating multiple asynchronous tests with act function jest js

in this case I am doing an example project which tries to show a phrase and the author of the phrase the first time the component is loaded and when a button is clicked to obtain a new phrase. The problem with testing is I am trying to simulate the way the user interacts with the application and having two tests asynchronously where each one has the act () function I get the following error:
console.error node_modules/react-dom/cjs/react-dom-test-utils.development.js:87
Warning: You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one.
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to QuoteContainer inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
in QuoteContainer (created by WrapperComponent)
in WrapperComponent
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to QuoteContainer inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
in QuoteContainer (created by WrapperComponent)
in WrapperComponent
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to QuoteContainer inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
in QuoteContainer (created by WrapperComponent)
in WrapperComponent
The test is passing but it generates this alert. I already looked for alternatives such as avoiding asynchronous testing but when doing it the test does not pass, it forces me to put the await in the act.
This example is simple but I would like to apply this same test in more complex applications to verify the correct operation of it. So it is probably necessary to have more asynchronous tests.
Here is the container component:
import React, {
useEffect,
useState,
} from "react";
import Quote from "./quote.component";
import { getQuote } from "../../repository/quote.repository";
const QuoteContainer = () => {
const [quote, setQuote] = useState("");
const [author, setAuthor] = useState("");
const [isLoading, setLoading] = useState(true);
useEffect(() => {
getQuoteData();
}, []);
const newQuoteHandler = () => {
getQuoteData();
};
const getQuoteData = async () => {
const {
quoteText,
quoteAuthor,
} = await getQuote();
setAuthor(quoteAuthor);
setQuote(quoteText);
setLoading(false);
};
return (
<Quote
isLoading={isLoading}
quote={quote}
author={author}
newQuoteHandler={newQuoteHandler}
/>
);
};
export default QuoteContainer;
And the test related to the container:
import React from "react";
import { server, rest } from "../../mocks/server";
import waitForExpect from "wait-for-expect";
import { mount } from "../../enzymeConfig";
import QuoteContainer from "./quote.container";
import { act } from "react-dom/test-utils";
import { QuoteText } from "../quote/quote.style";
import { SpinnerContainer } from "../withSpinner/withSpinner.style";
describe("testing quote api", () => {
it("should render spinner on start", () => {
const wrapper = mount(<QuoteContainer />);
expect(
wrapper.find(SpinnerContainer)
).toHaveLength(1);
});
it("should render actual component on load information", async (done) => {
expect.assertions(1);
const wrapper = mount(<QuoteContainer />);
await act(async () => {
await waitForExpect(() => {
wrapper.update();
expect(
wrapper.find(QuoteText).text()
).toEqual(
"Know how to listen, and you will profit even from those who talk badly."
);
done();
});
});
});
it("should change quote and author when Quote Button click", async (done) => {
const wrapper = mount(<QuoteContainer />);
await act(async () => {
await waitForExpect(() => {
wrapper.update();
expect(
wrapper.find(QuoteText).text()
).toEqual(
"Know how to listen, and you will profit even from those who talk badly."
);
done();
});
});
rest.get(
"apiURL",
(req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
quoteText: "this is an other frase",
quoteAuthor: "Plutarch ",
senderName: "",
senderLink: "",
})
)
);
wrapper
.find(CustomButton)
.at(0)
.simulate("click");
await act(async () => {
await waitForExpect(() => {
wrapper.update();
expect(
wrapper.find(QuoteText).text()
).toEqual("this is an other frase");
done();
});
});
});
});
Thanks from now

Resources