Flushing a Promise queue? - reactjs

While writing tests using Jest, an asynchronous mocked Axios request was causing a setState() call after a component was mounted. This resulted in console output during the test run.
it('renders without crashing', () => {
const mockAxios = new MockAdapter(axios);
mockAxios.onGet(logsURL).reply(200, [...axios_logs_simple]);
const div = document.createElement('div');
ReactDOM.render(<Logs />, div);
ReactDOM.unmountComponentAtNode(div);
});
And in the <Logs> component:
componentDidMount() {
axios.get(apiURL).then(response => {
this.setState({data: response.data});
});
}
I found a blog post showing a solution, but I don't understand how it does anything useful:
it('renders without crashing', async () => { //<-- async added here
const mockAxios = new MockAdapter(axios);
mockAxios.onGet(logsURL).reply(200, [...axios_logs_simple]);
const div = document.createElement('div');
ReactDOM.render(<Logs />, div);
await flushPromises(); //<-- and this line here.
ReactDOM.unmountComponentAtNode(div);
});
const flushPromises = () => new Promise(resolve => setImmediate(resolve));
As I understand it, it creates a new promise that resolves immediately. How can that guarantee other asynchronous code will resolve?
Now, it solved the issue. There are no more console messages during the test run. The test still passes (it's not a very good one). But this just seems like I just landed on the other side of a race condition, rather than preventing the race condition in the first place.

I will start with the reason why do you need this solution.
The reason is that you don't have the reference to the async task and you need your test assertion to run after the async task.
If you had it, you can just await on it, this will assure that your test assertion will run after the async task. (in your case it is ReactDOM.unmountComponentAtNode)
In your example the async task is inside componentDidMount which react calls, so you don't have the reference to the async task.
Usually a simple implementation of flushPromises will be:
const flushPromises = () => new Promise(resolve => setImmediate(resolve));
This implementation is based on the fact that async operations in javascript are based on task queue (javascript has several types of queues for async tasks).
Specifically, promises are queued in a micro-task queue. Each time an async operation (such as http request) finishes the async task dequeues and executes.
setImmediate is a method that gets a callback and stores it on the Immediates Queue and will be called in the next iteration of the event loop.
This Immediates Queue is checked after the Micro-task queue (which holds the promises).
Let's analyze the code, we are creating a new promise which is enqueued at the end of the Micro-task queue, it's resolve will be called on the next iteration of the event loop, that means it will be resolved after all the promises that are already enqueued.
Hope this explanation helps.
Pay attention that if inside of the async task you will enqueue a new promise, it will enter the queue at the end, that means that the promise that flushPromises return will not run after it.
Few posts / videos for more info:
https://blog.insiderattack.net/promises-next-ticks-and-immediates-nodejs-event-loop-part-3-9226cbe7a6aa
https://www.youtube.com/watch?v=8aGhZQkoFbQ
https://www.youtube.com/watch?v=cCOL7MC4Pl0

Related

async await not working as expected with react

I am calling an async function inside useEffect hook in react as below.
const ab = async () => {
console.log("start API");
await callAPI();
console.log("end API");
};
useEffect(() => {
ab();
console.log("calling");
}, []);
As the output first it will print start API then print calling and finally print end API. But what I need as the printing order is start API, end API, and calling. That's why I am using async await as well. But it's not working as expected. Any way to fix this issue?
That's the point of using await – the execution is paused until callAPI is finished.
To achieve the order you want, don't use await and just let the code run asynchronously. This way, the synchronous code (console.log('calling')) will be ran without waiting for the asynchronous code (callAPI()) to finish.
EDIT: However, depending on your use case, the other answer would probably make more sense.
For async/await to work , you'll have to await an asynchronous function.
Hence use the following code.
useEffect(() => {
let helper = async () => {
await ab(); // write await here
console.log('calling');
};
helper();
}, []);
Now the output would be:
Start API
End API
calling

Test functionality with timeouts in react-testing-library and jest

I'm trying to follow TDD, and I have a span that should appear on screen after 5 seconds. I haven't implemented the span at all, so the test should fail, but currently it passes the test expect(messageSpan).toBeInTheDocument.
Here are my two tests:
it("doesn't show cta message at first", () => {
render(<FAB />);
const messageSpan = screen.queryByText(
"Considering a career in nursing? Join our team!"
);
expect(messageSpan).toBeNull(); // passes, as it should
});
it("should show the cta message after 5 secs", () => {
render(<FAB />);
setTimeout(() => {
const messageSpan = screen.getByText( // also tried queryByText instead of get
"Considering a career in nursing? Join our team!"
);
expect(messageSpan).toBeInTheDocument(); // also passes, even though messageSpan should throw an error.
}, 5000);
});
Here's my FAB component, where you can see there's no message at all:
export default function FAB() {
return (
// using styled-components; there's no content in any of these.
<StyledFABContainer>
<StyledFABButton>
<BriefcaseIcon />
</StyledFABButton>
</StyledFABContainer>
);
}
To complicate things, I don't plan to have a set function I call for the setTimeout. I will simply set state after a set time of 5 secs. So don't think I can use the suggestions in the timer mocks section of jest docs: https://jestjs.io/docs/timer-mocks
My two questions are:
a) Why would this pass and not throw an error/null?
b) How can I properly test setTimeout functionality in RTL?
UPDATE: Have tried using various combinations of useFakeTimers, act, waitFor etc., but no luck. Here's my current test as it's written out, and throwing two errors - one saying I need to use act when changing state (which I am, but still) and one saying my messageSpan is null:
it("cta message to display after 5 secs", async () => {
jest.useFakeTimers();
const el = document.createElement("div");
act(() => {
ReactDOM.render(<FAB />, el);
});
jest.advanceTimersByTime(5000);
const messageSpan = screen.queryByText(
"Considering a career in nursing? Join our team!"
);
expect(messageSpan).toBeInTheDocument();
});
You can not use setTimeout like this in your tests. An obvious reason would be that you do not want to wait 5 seconds in your test to then continue. Imagine a component that would change after 10min. You cant wait that long but should use jests mocked timers API instead.
You can progress the nodejs timer so your component changes, then immediately make your assertion.
Other remarks about the tests you wrote:
If you are awaiting changes you should use await waitFor(() => ...) from testing library. It checks the expectations to pass every 50ms by default for a total of 5sec. If all expectations pass it continues. You should make asynchronous assertions there.
"expect(messageSpan).toBeNull()" should be "expect(messageSpan).not.toBeInTheDocument()". Its good to go with the accessibility way testing library provides.
If working with an asynchronous test I found a solution on this blog: https://onestepcode.com/testing-library-user-event-with-fake-timers/
The trick is to set the delay option on the userEvent to null.
const user = userEvent.setup({ delay: null });
Here is a full test case
test("Pressing the button hides the text (fake timers)", async () => {
const user = userEvent.setup({ delay: null });
jest.useFakeTimers();
render(<Demo />);
const button = screen.getByRole("button");
await user.click(button);
act(() => {
jest.runAllTimers();
});
const text = screen.queryByText("Hello World!");
expect(text).not.toBeInTheDocument();
jest.useRealTimers();
});

Call multiple async functions using useEffect

I have the following function that makes a call to the api
useEffect(() => {
loadMarkets();
}, []);
async function loadMarkets() {
const marketInformation = await frService.getMarketInfo();
addMarketInfo(marketInformation.response.data);
}
I want to make a similar call to loadBooks(). If I don't mind which method comes first, would I be able to just call it like below or should I be wrapping this in 1 async function/wrap it in a Promise?
useEffect(() => {
loadMarkets();
loadBooks();
}, []);
async function loadMarkets() {
const marketInformation = await frService.getMarketInfo();
addMarketInfo(marketInformation.response.data);
}
async function loadBooks() {
....
loadMarkets();
loadBooks();
because these function are prefixed with the async keyword, these are promise. loadBooks will be fired before loadMarkets finishes.
simple answer, yes you can do as you wrote, however I would try to catch the errors with something like
loadMarkets().catch(doSomethingIfThereIsAnError)
where doSomethingIfThereIsAnError would be a function that does som...
It doesn't really matter which comes first since both of them are async. During the initial rendering of your component, useEffect will invoke both async functions. If you need to check which api endpoint gets called first and how much time it takes for the execution, you can open browser dev tools and check the network tab.

React Testing Library - using 'await wait()' after fireEvent

I'm trying to use Testing Library to check for DOM Elements after a fireEvent.click. I know I need to wait after the fireEvent, but am not sure why simply using await doesn't work? Below is the same test written two ways -- the first one fails, the second passes. I don't understand why the first one fails...am very grateful for any insights!
p.s. -- I know wait is deprecated and waitFor is preferred, however due to some constraints I can not update the version at this time :(
FAILING TEST
// This test fails with the following error and warning:
// Error: Unable to find an element by: [data-test="name_wrapper"]
// Warning: An update to OnlinePaymentModule inside a test was not wrapped in act(...).
it('this is a failing test...why', async () => {
const { getByText, getByTestId } = render(<Modal {...props} />);
const button = getByText('open modal');
fireEvent.click(button);
const nameWrapper = await getByTestId('name_wrapper');
expect(
nameWrapper.getElementsByTagName('output')[0].textContent
).toBe('Jon Doe');
const numberWrapper = await getByTestId('number_wrapper');
expect(
numberWrapper.getElementsByTagName('output')[0].textContent
).toBe('123456');
});
PASSING TEST -- Why does this pass but first one fails?
// This test passes with no warnings
it('this is a passing test...why', async () => {
const { getByText, getByTestId } = render(<Modal {...props} />);
const button = getByText('open modal');
fireEvent.click(button);
await wait(() => {
const nameWrapper = getByTestId('name_wrapper');
expect(
nameWrapper.getElementsByTagName('output')[0].textContent
).toBe('Jon Doe');
const numberWrapper = getByTestId('number_wrapper');
expect(
numberWrapper.getElementsByTagName('output')[0].textContent
).toBe('123456');
})
});
5 months later I'm coming back to answer my question (I've learned a lot since posting this question lol)....
First of all, since it is 5 months later I want to underscore that it is better to use the userEvent library instead of fireEvent if possible.
I also would be remiss to not call out that there are a lot of antipatterns in the code ...You should only ever make one assertion in waitFor. You should avoid using getByTestId in favor of more accessible alternatives.
And finally the reason the first test was failing is that you can not use wait with getBy*. getBy is not async and will not wait. This would have been the better solution:
fireEvent.click(button);
const nameWrapper = await findByTestId('name_wrapper');
Then the test would have waited on the nameWrapper element to be available.
The second test passed because getBy is wrapped in RTL's async utility, wait (wait is now deprecated in favor of waitFor). That is essentially what findBy does under the hood -- findBy is the async version of getBy.
When I posted the question I didn't fully understand that await is a Javascript key word (and just syntactical sugar to make code wait on a promise to resolve). wait (now waitFor) is a utility from RTL that will make execution of the test wait until the callback does not throw an error.

Jest: Wait for called .then of async method in componentDidMount

I am currently stuck on writing a test of my React-App.
I have an async call in my componentDidMount method and are updating the state after returning. However, I do not get this to work.
I have found several solutions and None seems to work as expected. Below is the nearest point I have come to.
App:
class App extends Component<{}, IState> {
state: IState = {
fetchedData: false
};
async componentDidMount() {
await thing.initialize();
this.test();
}
test = () => {
this.setState({ fetchedData: true })
};
render() {
return this.state.fetchedData ? (
<div>Hello</div>
) : (
<Spinner />
);
}
}
The test
it('Base test for app', async () => {
const spy = spyOn(thing, 'initialize').and.callThrough(); // just for debugging
const wrapper = await mount(<App />);
// await wrapper.instance().componentDidMount(); // With this it works, but componentDidMount is called twice.
wrapper.update();
expect(wrapper.find('Spinner').length).toBe(0);
});
Well, so...thing.initialize is called (it is an async method that fetches some stuff).
If I do explicitly call wrapper.instance().componentDidMount() then it will work, but componentDidMount will be called twice.
Here are my ideas that I have tried but None succeeded:
Spying on thing.initialize() -> I did not find out how I proceed with the test after the method has been called and finished.
Spying on App.test -> The same here
Working with promises instead of async await
At the beginning, I had an thing.initialize().then(this.test) in my componentDidMount
It can't be much, but can someone tell me which piece I am missing?
if this is integration test you better to follow awaiting approach that say Selenium use: that is, just wait until some element appears or timeout reached. How it should be coded depends on library you use(for Puppeter it should be waitForSelector).
Once it's about unit test then I suggest you different approach:
mock every single external dependencies with Promise you control(by your code it's hard to say if automatic mock will work or you need to compose mock factory but one of them or both will help)
initialize element(I mean just run shallow() or mount())
await till your mocks are resolved(with extra await, using setTimeout(... ,0) or flush-promises will work, check how microtasks/macrotasks works)
assert against element's render and check if your mocks has been called
And finally:
setting state directly
mocking/spying on internal methods
verifying against state
are all lead to unstable test since it's implementation details you should not worry about during unit-testing. And it's hard to work with them anyway.
So your test would look like:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('loads data and renders it', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.resolve(/*data you expect to return*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(1);
})
or you may test how component behaves on rejection:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('renders error message on loading failuer', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.reject(/*some error data*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
expect(wrapper.find(SomeErrorMessage)).toHaveLength(1);
})

Resources