I might be missing something in here, I'm trying to mock an API function inside of my useEffect hook.
The API call should return a version number (i.e 1.0.0)
I tried to mock and sent it to the component, but for some reason, it's not working.
My useEffect hook:
useEffect(() => {
async function loader() {
const ver = await loadVersion();
if (ver === 401) {
history.push("/login");
return;
}
setVersion(ver);
};
loader();
}, []);
My test:
describe("Footer", () => {
const loader = jest.fn();
const loadVersion = jest.fn(() => '2.0.0');
let wrapper;
beforeEach(() => {
wrapper = shallow(<Footer loader={loader} loadVersion={loadVersion}/>)
})
it("Renders without crashing", () => {
expect.assertions(1)
expect(wrapper).not.toBeNull();
wrapper.update();
console.log(wrapper.debug()) // I'm trying to get the version from mocked loadVersion.
})
})
I tried to debug it to check if it is getting to mocked function but it's not, and in my coverage report it is not passing the first line of the async function inside the useEffect hook.
Related
tldr: the await call inside a useEffect hook doesn't resolve itself until after the component starts to unmount, it just hangs until that happens. Not sure why this is happening or how to debug it. This is in a react-native expo project. Swapping the functional component out with a class based one works as expected.
given the following useEffect calls in an expo project
useEffect(() => {
console.log('mount');
return () => {
console.log('unmount');
};
}, []);
useEffect(() => {
const fetch = async () => {
console.log('fetching')
const stuff = await fetchStuff();
console.log('fetched');
};
fetch();
}, [depA, depB]);
What I'm seeing in the console when the component is mounted is
'mount'
'fetching'
then when the component is unmounted I see
'unmount'
'fetched'
For some reason, the await call doesn't resolve until the component is unmounted. I've used this pattern in other parts of my code seemingly without issue so I can't figure out why this is happening here. When I swap the functional component out with a class it's working as expected. Any ideas on why this is happening? It looks like the fetchStuff call is being deferred until the component is about to unmount. Swapping fetchStuff out with await new Promise((res) => res(null)); doesn't seem to make any difference
Full component looks something like
function WhatIsHappening({depA, depB}) {
const [stuff, setStuff] = useState([])
useEffect(() => {
console.log('mount');
return () => {
console.log('unmount');
};
}, []);
useEffect(() => {
const fetch = async () => {
console.log('fetching')
const stuff = await fetchStuff(depA, depB);
console.log('fetched');
setStuff(stuff)
};
fetch();
}, [depA, depB]);
return (
<View>
<ListStuff stuff={stuff}></ListStuff>
<View>
)
}
There is something wrong with fetchStuff. This is a working version.
async function fetchStuff() {
return new Promise((resolve) => resolve("fetched"));
}
Working Sandbox
I am trying unsuccessfully to mock a Promise:
utils.js
export const fetching = () => {
const data = [1,2,3];
return new Promise(resolve=>resolve(data));
}
MyComponent
export function MyComponent() {
...
useEffect(()=>{
async function callback() {
const fetchedData = await fetching();
}
callback();
}, []);
return (
...
);
}
MyTest
import * as myStuff from ...;
jest.spyOn(myStuff, 'fetching').mockImplementation(()=>Promise.resolve([5]))
test('should ...', async () => {
// given
render(<MyComponent/>);
});
What am I getting as fetchedData by running the above test is undefined instead of [5]. Any idea why?
You're using the call in useEffect which means it's triggering after the component is actually mounted. Your test is async, but you're not advancing timers. Try:
jest.useFakeTimers()
const fetchSpy = jest.spyOn(myStuff, 'fetching').mockImplementation(()=>Promise.resolve([5]))
test('should ...', async () => {
// given
render(<MyComponent/>);
// here, we essentially push time forward, after useEffect has been triggered by the mounted component
act(() => {
jest.advanceTimersByTime(1000)
})
expect(fetchSpy).toBeCalled()
});
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"
});
I have a React function component.
I pass a function to the component as a prop, that returns a promise.
I use that function on an onClick event and once the promise is resolved, I change the state of the component.
Something like:
import React, { useState } from 'react';
function myComponent({ aPromiseReturningFunction }) {
const [myState, setState] = useState('12');
const clickHandler = () => {
aPromiseReturningFunction().then(() => { setState('123') })
};
return <div onClick={ clickHandler }>{myState}</div>
}
Inside my test:
const myFunc = jest.fn(() => Promise.resolve(true));
const componentWrapper = shallow(<myComponent aPromiseReturningFunction={ myFunc }/>);
componentWrapper.simulate('click');
expect(componentWrapper.text()).toEqual('123');
Obviously the above fails, but I have not found anything that would explain how to properly test the above. Of course If I change the state outside the promise, the test passes.
Any suggestions?
Since click is updating the state after a promise aka asynchronously, I would use act
import { act } from 'react-dom/test-utils'; // other testing libraries have similar methods that test async events
const myFunc = jest.fn(() => Promise.resolve(true));
it('updates text after onclick', () => {
const componentWrapper = shallow(<myComponent aPromiseReturningFunction={ myFunc }/>);
act(() => {
componentWrapper.simulate('click');
});
expect(componentWrapper.text()).toEqual('123');
});
Thanks to alextrastero, I managed to come to a solution eventually.
What is missing from alextrastero's answer is that we should enclose the act() inside async/await like:
import { act } from 'react-dom/test-utils'; // other testing libraries have similar methods that test async events
const myFunc = jest.fn(() => Promise.resolve(true));
it('updates text after onclick', async () => {
const componentWrapper = shallow(<myComponent aPromiseReturningFunction={ myFunc }/>);
await act(() => {
componentWrapper.simulate('click');
});
expect(componentWrapper.text()).toEqual('123');
});
And in order for that to work, I also needed to use the regenerator-runtime/runtime package.
In my test, the component receives its props and sets up the component.
This triggers a useEffect to make an http request (which I mock).
The fetched mocked resp data is returned, but the cleanup function inside the useEffect has already been called (hence the component has unmounted), so I get all these errors.
How do I prevent the component from un-mounting so that the state can be updated? I've tried act, no act, nothing causes the component to wait for the fetch to finish.
I should say my warning are just that, warnings, but I don't like all the red, and it indicates something is going wrong.
export const BalanceModule = (props) => {
const [report, setReport] = useState();
useEffect(() => {
fetch('http://.....').then((resp) => {
console.log("data returned!!!")
setReports((report) => {
return {...report, data: resp}
})
})
return () => {
console.log("unmounted!!!")
};
}, [report])
.... trigger update on report here
}
// the test:
test("simplified-version", async () => {
act(() => {
render(
<BalanceModule {...reportConfig}></BalanceModule>
);
});
await screen.findByText("2021-01-20T01:04:38");
expect(screen.getByText("2021-01-20T01:04:38")).toBeTruthy();
});
Try this:
test("simplified-version", async () => {
act(() => {
render(<BalanceModule {...reportConfig}></BalanceModule>);
});
await waitFor(() => {
screen.findByText("2021-01-20T01:04:38");
expect(screen.getByText("2021-01-20T01:04:38")).toBeTruthy();
});
});