Mocking data returned by useQuery using MockedProvider - reactjs

I have a simple component. All it does is fetch data using useQuery and pass it to another component. The component works fine, but I am unable to test it without adding hacks as described here:
Testing Final Stage
I have had a look at MockedProvider requires timeout, and this was asked 2.5 years back. Is there any other way available? I can't believe the library team advocates putting wait/setTimeout in units!
Component:
export const PROFILE_QUERY = gql`
query {
profile {
roles
}
}
`;
export const Connected = () => {
const { loading, data, error } = useQuery(PROFILE_QUERY);
const setCurrentProfile = (role: string) => {
cachedSettings(getSettings(role));
};
const roles = data?.profile?.roles;
return <Profile {...{ roles, loading, error, setCurrentProfile }} />;
};
Test Case:
import React from 'react';
import { render } from '#testing-library/react';
import { MemoryRouter } from 'react-router';
import Profile from '../Profile';
import ConnectedProfile from '..';
import { MockedProvider, MockedResponse } from '#apollo/client/testing';
import { PROFILE_QUERY } from '../Profile.connected';
jest.mock('../Profile', () => {
return jest.fn(() => null);
});
describe('Connected <Profile />', () => {
const renderComponent = (roles: string[] | undefined) => {
const mock: MockedResponse = {
request: {
query: PROFILE_QUERY
},
result: {
data: {
profile: {
roles
}
}
}
};
return render(
<MockedProvider mocks={[mock]} addTypename={false}>
<MemoryRouter>
<ConnectedProfile />
</MemoryRouter>
</MockedProvider>
);
};
it('multiple roles must have been passed', async () => {
renderComponent(['foo', 'bar']);
**//DOCUMENTATION says do following? - will have to wrap rendering in act as well
//await new Promise(resolve => setTimeout(resolve, 0));**
const args = (Profile as jest.Mock).mock.calls[0][0];
expect(args.roles).toEqual(['agent', 'administrator']);
});
it('no roles passed', async () => {
renderComponent(undefined);
//DOCUMENTATION says do following?
//await new Promise(resolve => setTimeout(resolve, 0));
const args = (Profile as jest.Mock).mock.calls[0][0];
expect(args.roles).toEqual(undefined);
});
afterEach(() => (Profile as jest.Mock).mockClear());
});

If MockedProvider is asynchronous and contains one-tick delay, a requirement to delay at least for the same amount of time is reasonable. This is a common issue for asynchronous tests and this is what popular flush-promises utility does.
React Testing Library is focused on blackbox testing and encourages the use of waitFor helper to not rely on the implementation. It may result in accumulated test time due to polling intervals:
renderComponent(undefined);
await waitFor(() => {
expect(Profile).toBeCalledTimes(1);
expect(Profile).toBeCalledWith(expect.objectContaining({ roles: undefined }));
});
Since this MockedProvider behaviour is known and documented, it can be safely implemented in renderComponent helper itself:
const renderComponent = async (roles: string[] | undefined) => {
...
const result = render(
<MockedProvider mocks={[mock]} addTypename={false}>...</MockedProvider>
);
await new Promise(resolve => setTimeout(resolve));
return result;
};
...
await renderComponent(undefined);
expect(Profile).toBeCalledTimes(1);
expect(Profile).toBeCalledWith(expect.objectContaining({ roles: undefined }));

Related

How to test a component that is conditionally rendered based on a hook value?

I am working on a React Native application and am very new to testing. I am trying to mock a hook that returns a true or false boolean based on the current user state. I need to mock the return value of the authState variable, and based on that, I should check if the component is rendered or not. But the jest mock is returning the same value only
useAuth.ts
export const useAuthState = () => {
const [authState, setAuthState] = useState<AuthState>();
useEffect(() => {
return authentication.subscribe(setAuthState);
}, []);
return authState;
};
MyComponent.tsx
export const MyComponent = () => {
const authState = useAuthState();
if (!authState) {
return null;
}
return <AnotherComponent />
}
MyComponent.test.tsx
import { MyComponent } from "./MyComponent"
jest.mock('../use-auth-state', () => {
return {
useAuthState: () => false,
};
});
const TestComponent = () => <MyComponent />
describe('MyComponent', () => {
it('Should return null if the authState is null', () => {
let testRenderer: ReactTestRenderer;
act(() => {
testRenderer = create(<TestComponent />);
});
const testInstance = testRenderer.getInstance();
expect(testInstance).toBeNull()
})
})
This is working fine. But, I am not able to mock useAuthState to be true as this false test case is failing. Am I doing it right? I feel like I am messing up something.
You want to change how useAuthState is mocked between tests, right? You can set your mock up as a spy instead and change the mock implementation between tests.
It's also a little more ergonomic to use the render method from react-testing-library. The easiest way would be to give your component a test ID and query for it. Something like the below
import { MyComponent } from "./MyComponent"
import * as useAuthState from '../use-auth-state';
const authStateSpy = jest.spyOn(useAuthState, 'default');
describe('MyComponent', () => {
it('Should return null if the authState is null', () => {
// you can use .mockImplementation at any time to change the mock behavior
authStateSpy.mockImplementation(() => false);
const { queryByTestId } = render(<MyComponent />;
expect(queryByTestId('testID')).toBeNull();
})

How to test a component that fetches data in useEffect and stores it in state with react-testing-library and jest?

I'm fairly new to react-testing-library and generally testing. I want to test a component that fetches the data from an API in useEffect hook. Then it stores it in local state. It renders these array data with array.map, but i'm getting Error: Uncaught [TypeError: Cannot read properties of undefined (reading 'map')] error. I'm probably doing wrong in my test suite, i've researched a lot but couldn't fix it.
import React from 'react';
import { render, screen } from '#testing-library/react';
import '#testing-library/jest-dom'
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { OnePiece } from '.';
const server = setupServer(rest.get('server http address', (req, res, ctx) => {
const totalData = [
{ name: "doffy", price: 100, image: "image url" },
{ name: "lamingo", price: 500, image: "image url" }
];
return res(
ctx.status(200),
ctx.json({
data: { crew: totalData }
})
)
}))
beforeAll(() => server.listen());
afterAll(() => server.close());
beforeEach(() => server.restoreHandlers());
//console.log("mocking axios", axios)
describe('OnePiece', () => {
test('fetches the data from the API and correctly renders it', async () => {
//Here's probably where i fail. Please someone tell me the right way :)
await render(<OnePiece />)
const items = await screen.findAllByAltText('product-image');
expect(items).toHaveLength(2);
// screen.debug()
})
})
And the below is the parts of code useEffect, and totalData.map in the component:
const [totalData, setTotalData] = useState([]);
const [crew, setCrew] = useState('straw-hat-pirates');
useEffect(() => {
let isApiSubscribed = true;
const getOpData = async () => {
const getCrews = await axios.get('http address');
if (isApiSubscribed) {
let data = getCrews.data;
data = data[crew];
// console.log("data", data);
setTotalData(data);
}
}
getOpData();
return () => {
isApiSubscribed=false;
}
}, [crew])
.........
//in the return part
<ProductsWrapper>
{totalData.map((product, index) =>
<ProductCard key={index} name={product.name} price={product.price} imageUrl={product.image} />
)}
</ProductsWrapper>
As i predicted, the problem was the async data fetching. Currently setTimeOut is more than enough for me, but if someone sees this in the future, you can look for the waitFor method of react-testing-library.
Here's the fixed part:
describe('OnePiece', () => {
test('fetches the data from the API and correctly renders it', async () => {
render(<OnePiece />)
setTimeout(async () => {
const items = await screen.findAllByAltText('product-image');
expect(items).toHaveLength(2);
}, 4000)
//screen.debug()
})
})

Need do write unit tests with jest for react app, that uses okta, but getting warning

import { SecureRoute, Security, LoginCallback } from '#okta/okta-react';
import React, { useMemo } from 'react';
import { Route, Switch } from 'react-router-dom';
import { OktaAuth, toRelativeUrl } from '#okta/okta-auth-js';
import Comp from './Comp';
const config = {oidc: {....}};
const AppRouter = () => {
const oktaAuth = useMemo(() => new OktaAuth(config.oidc), []);
const restoreOriginalUri = async (sth, originalUri) => {
history.replace(toRelativeUrl(originalUri || '/', 'some-path-here'));
};
return (
<Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
<Switch>
<SecureRoute path="/" exact component={Comp} />
<Route path="/login/callback" component={LoginCallback} />
</Switch>
</Security>
);
};
export default AppRouter;
I have this in my app... how to write unit tests for it if I have the following warning?
Warning: An update to Security inside a test was not wrapped in act(...).
The waitFor thing doesn't seem to work. The okta Security, SecureRoute, and LoginCallback, all trigger test failures (element is undefined, etc).
I got it running this way: I figure I don't need to test any of those elements, I just need to test if my app is rendering. So I mocked everything and this passed the test without undue hackery.
Hope this helps, I see a lot of people out there struggle with this one:
import {cleanup, screen, waitFor} from '#testing-library/react';
import App from './App';
import {render} from "react-dom";
import {BrowserRouter} from "react-router-dom";
jest.mock('#okta/okta-react', () => {
return ({
useOktaAuth: () => ({
authState: {isAuthenticated: true},
authService: {handleAuthentication: jest.fn()},
oktaAuth: {getUser: () => new Promise((resolve, reject) => { resolve('foo')})},
}),
withOktaAuth: (x: any) => x,
Security: () => <div></div>,
SecureRoute: () => <div></div>,
LoginCallback: () => <div></div>
});
});
describe('<App />', () => {
let container: any = null;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(cleanup);
test("Render App", async () => {
render(<BrowserRouter><App /></BrowserRouter>, container);
const comp = await screen.findByTestId('App', container);
expect(comp).toBeInTheDocument();
});
});
The thing is you have to await for Okta asynchronous updates in the component.
You could just wrap your "expects" block with await waitFor(() => { } assuming you're using React Testing Library.
Example:
await waitFor(() => {
expect(getAllByText(/Home/i)[0]).toBeInTheDocument;
expect(getAllByText(/Home/i)[1]).toBeInTheDocument;
});

How to test a React component having async functions with jest & enzyme

I have a component:
RandomGif.js
import React, { Component } from "react";
import Gif from "./Gif";
import Loader from "./library/Loader";
import { fetchRandom } from "../resources/api";
class RandomGif extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
state = {
loading: false,
gif: null
};
componentDidMount() {
this.handleClick();
}
async handleClick() {
let gifContent = null;
try {
this.setState({
loading: true
});
const result = await fetchRandom();
if (!!result && result.data) {
gifContent = {
id: result.data.id,
imageUrl: result.data.images.downsized_large.url,
staticImageUrl: result.data.images.downsized_still.url,
title: result.data.title
};
}
} catch (e) {
console.error(e);
} finally {
this.setState({
loading: false,
gif: gifContent
});
}
}
render() {
const { gif, loading } = this.state;
const showResults = gif && !loading;
return (
<div className="random">
{!showResults && <Loader />}
<button className="btn" onClick={this.handleClick}>
RANDOMISE
</button>
{showResults && <Gif data={gif} />}
</div>
);
}
}
export default RandomGif;
If I call methods directly from the instance of this component, I can successfully test that the state is being updated. However, If I simulate a button click, nothing gets updated and the test fails. I've tried setImmediate and setTimeout tricks but those are not working.
So far I've not able to write a test case for:
Simulating button click.
Simulating lifecycle method.
This is what I've come up with so far.
RandomGif.spec.js
import React from "react";
import { shallow, mount } from "enzyme";
import RandomGif from "./RandomGif";
describe("Generate Random Gif", () => {
it("should render correctly.", () => {
const wrapper = shallow(<RandomGif />);
expect(wrapper).toMatchSnapshot();
});
it("should load a random GIF on calling handleSearch fn.", async () => {
const wrapper = mount(<RandomGif />);
const instance = wrapper.instance();
expect(wrapper.state("gif")).toBe(null);
await instance.handleClick();
expect(wrapper.state("gif")).not.toBe(null);
});
it("THIS TEST FAILS!!!", () => {
const wrapper = mount(<RandomGif />);
expect(wrapper.state("gif")).toBe(null);
wrapper.find('button').simulate('click');
wrapper.update()
expect(wrapper.state("gif")).not.toBe(null);
});
});
api.py
export const fetchRandom = async () => {
const url = `some_url`;
try {
const response = await fetch(url);
return await response.json();
} catch (e) {
console.error(e);
}
return null;
};
Please help me figure out the missing pieces of a puzzle called 'frontend testing'.
We need to mock fetchRandom so no real request will be sent during testing.
import { fetchRandom } from "../resources/api";
jest.mock("../resources/api"); // due to automocking fetchRandom is jest.fn()
// somewhere in the it()
fetchRandom.mockReturnValue(Promise.resolve({ data: { images: ..., title: ..., id: ...} }))
Since mocking is a Promise(resolved - but still promise) we need either setTimeout or await <anything> to make component's code realized this Promise has been resolved. It's all about microtasks/macrotasks queue.
wrapper.find('button').simulate('click');
await Promise.resolve();
// component has already been updated here
or
it("test something" , (done) => {
wrapper.find('button').simulate('click');
setTimeout(() => {
// do our checks on updated component
done();
}); // 0 by default, but it still works
})
Btw you've already did that with
await instance.handleClick();
but to me it looks the same magic as say
await 42;
And besides it works(look into link on microtasks/macrotasks) I believe that would make tests worse readable("what does handleClick return that we need to await on it?"). So I suggest use cumbersome but less confusing await Promise.resolve(); or even await undefined;
Referring to state and calling instance methods directly are both anti-patterns. Just a quote(by Kent C. Dodds I completely agree with):
In summary, if your test uses instance() or state(), know that you're testing things that the user couldn't possibly know about or even care about, which will take your tests further from giving you confidence that things will work when your user uses them.
Let's check rendering result instead:
import Loader from "./library/Loader";
...
wrapper.find('button').simulate('click');
expect(wrapper.find(Loader)).toHaveLength(1);
await Promise.resolve();
expect(wrapper.find(Loader)).toHaveLength(1);
expect(wrapper.find(Gif).prop("data")).toEqual(data_we_mocked_in_mock)
Let's get that altogether:
import {shallow} from "enzyme";
import Gif from "./Gif";
import Loader from "./library/Loader";
import { fetchRandom } from "../resources/api";
jest.mock( "../resources/api");
const someMockForFetchRandom = { data: { id: ..., images: ..., title: ... }};
it("shows loader while loading", async () => {
fetchRandom.mockReturnValue(Promise.resolve(someMockForFetchRandom));
const wrapper = shallow(<RandomGif />);
expect(wrapper.find(Loader)).toHaveLength(0);
wrapper.find('button').simulate('click');
expect(wrapper.find(Loader)).toHaveLength(1);
await Promise.resolve();
expect(wrapper.find(Loader)).toHaveLength(0);
});
it("renders images up to response", async () => {
fetchRandom.mockReturnValue(Promise.resolve(someMockForFetchRandom));
const wrapper = shallow(<RandomGif />);
wrapper.find('button').simulate('click');
expect(wrapper.find(Gif)).toHaveLength(0);
await Promise.resolve();
expect(wrapper.find(Gif).props()).toEqual( {
id: someMockForFetchRandom.data.id,
imageUrl: someMockForFetchRandom.data.images.downsized_large.url,
staticImageUrl: someMockForFetchRandom.data.images.downsized_still.url,
title: someMockForFetchRandom.data.title
});
});

Enzyme onclick spy toHaveBeenCalled test does not work when testing on arrow function

how can i test the child component onclick.
Please see the below snippet.
// App.js
import React, {Component, Fragment} from 'react'
import Child from './child'
class App extends Component{
state = {
data: null,
enable: false
}
componentDidMount(){
this.getData()
}
getData = async () => {
const response = await fetch('http://www.example.com');
const data = await response.json();
this.setState({
data
})
}
_handleChildClick = () => {
this.setState({
enable: true
})
}
render(){
const {data, enable} = this.state
if(!data){
return (
<div>
Loading
</div>
)
}else{
<Fragment>
<Child
handleChildClick={this._handleChildClick}
/>
</Fragment>
}
}
}
export default App
import React from 'react';
const child = () => {
return(
<div>
<button
className="toggle"
onClick={props.handleChildClick}
>
Toggle
</button>
</div>
)
}
export default child
// App.test.js
import React from 'react';
import {enzyme} from 'enzyme';
import App from './App';
describe("App test cases", () => {
it('should trigger _handleChildClick', async () => {
window.fetch = jest.fn().mockImplementation(() => ({
status: 200,
json: () => new Promise((resolve, reject) => {
resolve(
{
name: "some data"
}
)
})
}))
const mountWrapper = await mount(<App />)
setTimeout(() => {
mountWrapper.update()
const SpyhandleChildClick = jest.spyOn(mountWrapper.instance(),'_handleChildClick')
mountWrapper.find('.toggle').simulate('click')
expect(SpyhandleChildClick).toHaveBeenCalled() // not called
},0)
})
})
Some important points to consider.
Asynchronous code in your tests
If you have to do asynchronous tasks in your tests you always have to await until the asynchronous stuff is completed.
setTimeout(() => {
mountWrapper.update()
const SpyhandleChildClick = jest.spyOn(mountWrapper.instance(),'_handleChildClick')
mountWrapper.find('.toggle').simulate('click')
expect(SpyhandleChildClick).toHaveBeenCalled() // not called
},0)
Above in your code you have a timeout segment. Any test condition inside this code block will not be evaluated since by the time they are evaluated you 'test session' will already be over due to the aync nature.
Testing arrow functions in React with enzyme - forceUpdate()
There seem to be a problem with the enzyme library where you have to force update the react component after spying for it to latch on to the method.
Please follow the github issue for more information : https://github.com/airbnb/enzyme/issues/365
I also cleaned up your test code a bit to make it more understandable!
// App.test.js
import React from 'react';
import {enzyme} from 'enzyme';
import App from './App';
describe("App test cases", () => {
it("should trigger _handleChildClick", async () => {
window.fetch = jest.fn().mockImplementation(() => ({
status: 200,
json: () =>
new Promise((resolve, reject) => {
resolve({
name: "some data"
});
})
}));
const mountWrapper = mount(<App />);
mountWrapper.update();
console.log("mountWrapper", mountWrapper.debug()); // showing the loader one
//[FIX]This code will block and wait for your asynchronous tasks to be completed
await new Promise(res => setTimeout(() => res(), 0));
mountWrapper.update();
console.log("mountWrapper", mountWrapper.debug()); // nothing showing
expect(mountWrapper.find(".toggle").length).toEqual(1);
//[FIX]Get a reference from the wrapper and force update after the spyOn call
const instance = mountWrapper.instance();
const spy = jest.spyOn(instance, "_handleChildClick");
instance.forceUpdate();
mountWrapper.find(".toggle").simulate("click");
expect(spy).toHaveBeenCalled();
});
});
Live Demo Link: Click on the 'Tests' tab on the browser to see the test results
https://codesandbox.io/s/mz21kpm37j

Resources