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()
});
Related
I want to test a React component that, internally, uses a custom hook (Jest used). I successfully mock this hook but I can't find a way to test the calls on the functions that this hook returns.
Mocked hook
const useAutocomplete = () => {
return {
setQuery: () => {}
}
}
React component
import useAutocomplete from "#/hooks/useAutocomplete";
const MyComponent = () => {
const { setQuery } = useAutocomplete();
useEffect(() => {
setQuery({});
}, [])
...
}
Test
jest.mock("#/hooks/useAutocomplete");
it("sets the query with an empty object", () => {
render(<MyComponent />);
// I want to check the calls to setQuery here
// e.g. mockedSetQuery.mock.calls
});
CURRENT SOLUTION
I currently made the useAutocomplete hook an external dependency:
import useAutocomplete from "#/hooks/useAutocomplete";
const MyComponent = ({ autocompleteHook }) => {
const { setQuery } = autocompleteHook();
useEffect(() => {
setQuery({});
}, [])
...
}
MyConsole.defaultProps = {
autocompleteHook: useAutocomplete
}
And then I test like this:
const mockedSetQuery = jest.fn(() => {});
const useAutocomplete = () => ({
setQuery: mockedSetQuery,
});
it("Has access to mockedSetQuery", () => {
render(<MyComponent autocompleteHook={useAutocomplete} />);
// Do something
expect(mockedSetQuery.mock.calls.length).toBe(1);
})
You can mock the useAutocomplete's setQuery method to validate if it's invoked.
jest.mock("#/hooks/useAutocomplete");
it("sets the query with an empty object", () => {
const useAutocompleteMock = jest.requireMock("#/hooks/useAutocomplete");
const setQueryMock = jest.fn();
useAutocompleteMock.setQuery = setQueryMock;
render(<MyComponent />);
// The mock function is called twice
expect(setQueryMock.mock.calls.length).toBe(1);
});
I need to test the following component that consumes a custom hook of mine.
import { useMyHook } from 'hooks/useMyHook';
const MyComponent = () => {
const myHookObj = useMyHook();
const handler = () => {
myHookObj.myMethod(someValue)
}
return(
<button onClick={handler}>MyButton</button>
);
};
This is my test file:
jest.mock('hooks/useMyHook', () => {
return {
useMyHook: () => {
return {
myMethod: jest.fn(),
};
},
};
});
describe('<MyComponent />', () => {
it('calls the hook method when button is clicked', async () => {
render(<MyComponent {...props} />);
const button = screen.getByText('MyButton');
userEvent.click(button);
// Here I need to check that the `useMyHook.method`
// was called with some `value`
// How can I do this?
});
});
I need to check that the useMyHook.method was called with some value.
I also want to test it from multiple it cases and it might be called with different values on each test.
How can I do this?
This is how I was able to do it:
import { useMyHook } from 'hooks/useMyHook';
// Mock custom hook that it's used by the component
jest.mock('hooks/useMyHook', () => {
return {
useMyHook: jest.fn(),
};
});
// Mock the implementation of the `myMethod` method of the hook
// that is used by the Component
const myMethod = jest.fn();
(useMyHook as ReturnType<typeof jest.fn>).mockImplementation(() => {
return {
myMethod: myMethod,
};
});
// Reset mock state before each test
// Note: is needs to reset the mock call count
beforeEach(() => {
myMethod.mockReset();
});
Then, on the it clauses, I'm able to:
it (`does whatever`, async () => {
expect(myMethod).toHaveBeenCalledTimes(1);
expect(myMethod).toHaveBeenLastCalledWith(someValue);
});
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"
});
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();
});
});
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