Mocking React custom hook with Jest - reactjs

I need to mock my custom hook when unit testing React component. I have read few tutorials and stackoverflow answers to this simple task, but without luck to implement it correctly.
My simplest possible setup for single test is as following:
// TestComponent.js
import React from "react";
import useTest from "./useTest";
const TestComponent = () => {
const { state } = useTest("initial_value");
return <div>{state}</div>;
};
export default TestComponent;
// useTest.jsx - simple custom hook
import React, { useState } from "react";
const useTest = (initialState) => {
const [state] = useState(initialState);
return { state };
};
export default useTest;
// TestComponent.test.jsx - my test case
import React from "react";
import { render } from "#testing-library/react";
import TestComponent from "./TestComponent";
jest.mock("./useTest", () => ({
useTest: () => "mocked_value",
}));
test("rendertest", () => {
const component = render(<TestComponent />);
expect(component.container).toHaveTextContent("mocked_value");
});
So I trying to mock useTest custom hook to return "mocked_value", instead of "initial_value" from real custom hook. But above code just gives me this error:
TypeError: (0 , _useTest.default) is not a function
3 |
4 | const TestComponent = () => {
> 5 | const { state } = useTest("initial_value");
| ^
6 |
7 | return <div>{state}</div>;
8 | };
I have also tried:
import useTest from './useTest';
// ...
jest.spyOn(useTest, "useTest").mockImplementation(() => "mocked_value");
import useTest from './useTest';
// ...
jest.spyOn(useTest, "useTest").mockReturnValue("mocked_value");
But both gives me error Cannot spy the useTest property because it is not a function; undefined given instead.
How do I implement this test?

I'm answering to myself. This way it's working:
jest.mock("./useTest", () => ({
useTest: () => ({ state: 'mocked_value' }),
}));
And if I want to use default export in custom hook:
jest.mock("./useTest", () => ({
__esModule: true,
default: () => ({ state: 'mocked_value' }),
}));
Also, if I want to also use setState method in my hook and export it, I can mock it like this:
const mockedSetState = jest.fn();
jest.mock("./useTest", () => ({
useTest: () => ({ state, setState: mockedSetState }),
}));
And now it's possible to check if setState has been called once:
expect(mockedSetState).toHaveBeenCalledTimes(1);

Since you use export default useTest in useTest module, it expects to get that function reference from a default attribute in your mock.
Try this:
jest.mock("./useTest", () => ({
default: () => "mocked_value",
}));
If you want to avoid confusion, you could try export const useTest = ... in useTest module and then import { useTest } from './useTest' in your component. No need to change your test if using this approach.

For those who use the right hook it can be done this way
import * as useFetchMock from "../../../../hooks/useFetch";
it("should show loading component on button when state is loading", () => {
jest.spyOn(useFetchMock, "default").mockReturnValue({
result: null,
message: null,
status: "pending",
loading: true,
fetch: jest.fn(),
});
render(<Component />);
expect.assertions(1);
expect(screen.getByTestId(testIdComponentB)).toBeInTheDocument();
});
import your custom hook directly and use 'default' to interact directly

Related

Mocking zustand store for jest test

hej, maybe someone can help me with this? I try to mock a zustand store with custom values and actions. I'd like to do the mocking within my test file. But I fail miserably.
This is a simplified example of my react app.
component.jsx
import React from "react";
import useStore from "#app/component_store";
export default function Component() {
const foo = useStore((state) => state.foo);
const setFoo = useStore((state) => state.foo_set);
return (
<>
<div data-testid="foo">{foo}</div>
<button data-testid="btn-foo" onClick={() => setFoo("new foo")}>
set foo
</button>
</>
);
}
component_store.jsx
import create from "zustand";
const useStore = create((set) => ({
foo: "foo",
foo_set: (value) => set(() => ({ foo: value })),
}));
export default useStore;
component.test.jsx
/* global afterEach, expect, jest, test */
import React from "react";
import { cleanup, fireEvent, render } from "#testing-library/react";
import "#testing-library/jest-dom";
import mockCreate from "zustand";
import Component from "#app/component";
jest.mock("#app/component_store", () => (args) => {
const useStore = mockCreate((set, get) => ({
foo: "mocked foo",
foo_set: (value) => set(() => ({ foo: "mocked set foo" })),
}));
return useStore(args);
});
afterEach(cleanup);
test("override store values and actions", () => {
const { container, getByTestId } = render(<Component />);
const foo = getByTestId("foo");
const btnFoo = getByTestId("btn-foo");
expect(foo.textContent).toBe("mocked foo");
fireEvent.click(btnFoo);
expect(foo.textContent).toBe("mocked set foo");
});
This fails. expect(foo.textContent).toBe("mocked set foo"); will still have "mocked foo" as a value, although the mocked foo_set gets called (e.g. adding a console log does get logged). But again, the value of foo will not be updated.
However, if I move the store mocking part into its own file and import it into my test, everything will work as expected.
component_store_mocked.jsx
note: this is the same as in component.test.jsx
import create from "zustand";
const useStore = create((set, get) => ({
foo: "mocked foo",
foo_set: (value) => set(() => ({ foo: "mocked set foo" })),
}));
export default useStore;
updated component.test.jsx
/* global afterEach, expect, jest, test */
import React from "react";
import { cleanup, fireEvent, render } from "#testing-library/react";
import "#testing-library/jest-dom";
import mockCreate from "zustand";
import Component from "#app/component";
// this part here
import mockComponentStore from "#app/component_store_mocked";
jest.mock("#app/component_store", () => (args) => {
const useStore = mockComponentStore;
return useStore(args);
});
afterEach(cleanup);
test("override store values and actions", () => {
const { container, getByTestId } = render(<Component />);
const foo = getByTestId("foo");
const btnFoo = getByTestId("btn-foo");
expect(foo.textContent).toBe("mocked foo");
fireEvent.click(btnFoo);
expect(foo.textContent).toBe("mocked set foo");
});
I would love to do the mocking within the test file and not pollute my system with dozens of mocked stores for my test ... Again, this is just a simplified setup of a much larger app.

React component not setting state after mocking redux

Here is my test
const initialRootState = {
accounts: [mockAccounts],
isLoading: false
}
describe('Account Dashboard', () => {
let rootState = {
...initialRootState
}
const mockStore = configureStore()
const store = mockStore({ ...rootState })
const mockFunction = jest.fn()
jest.spyOn(Redux, 'useDispatch').mockImplementation(() => mockFunction)
jest
.spyOn(Redux, 'useSelector')
.mockImplementation((state) => state(store.getState()))
afterEach(() => {
mockFunction.mockClear()
// Reseting state
rootState = {
...initialRootState
}
})
it('renders correctly', () => {
const wrapper = mount(
<TestWrapper>
<AccountDashboard />
</TestWrapper>
)
console.log(wrapper)
})
})
In my component I am mapping accounts from the state. In my test I am getting the following error TypeError: Cannot read property 'map' of undefined
I would like to test an if statement I am using in my component to ensure it's returning the proper view based on the number of accounts I am receiving.
However, when I console.log(store.getState()) it is printing correctly. What am I doing wrong?
If you're going to test a Redux connected component, I'd recommend steering away from mocking its internals and instead to test it as if it were a React component connected to a real Redux store.
For example, here's a factory function for mounting connected components with enzyme:
utils/withRedux.jsx
import * as React from "react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { mount } from "enzyme";
import rootReducer from "../path/to/reducers";
/*
You can skip recreating this "store" by importing/exporting
the real "store" from wherever you defined it in your app
*/
export const store = createStore(rootReducer);
/**
* Factory function to create a mounted Redux connected wrapper for a React component
*
* #param {ReactNode} Component - the Component to be mounted via Enzyme
* #function createElement - Creates a wrapper around the passed in component with incoming props so that we can still use "wrapper.setProps" on the root
* #returns {ReactWrapper} - a mounted React component with a Redux store.
*/
export const withRedux = Component =>
mount(
React.createElement(props => (
<Provider store={store}>
{React.cloneElement(Component, props)}
</Provider>
)),
options
);
export default withRedux;
Now, using the above factory function, we can test the connected component by simply using store.dispatch:
tests/ConnectedComponent.jsx
import * as React from "react";
import withRedux, { store } from "../path/to/utils/withRedux";
import ConnectedComponent from "../index";
const fakeAccountData = [{...}, {...}, {...}];
describe("Connected Component", () => {
let wrapper;
beforeEach(() => {
wrapper = withRedux(<ConnectedComponent />);
});
it("initially shows a loading indicator", () => {
expect(wrapper.find(".loading-indicator")).exists().toBeTruthy();
});
it("displays the accounts when data is present", () => {
/*
Ideally, you'll be dispatching an action function for simplicity
For example: store.dispatch(setAccounts(fakeAccountData));
But for ease of readability, I've written it out below.
*/
store.dispatch({ type: "ACCOUNTS/LOADED", accounts: fakeAccountData }));
// update the component to reflect the prop changes
wrapper.update();
expect(wrapper.find(".loading-indicator")).exists().toBeFalsy();
expect(wrapper.find(".accounts").exists()).toBeTruthy();
});
});
This vastly simplifies not having to mock the store/useSelector/useDispatch over and over when you start to test other Redux connected components.
On a side note, you can skip this entirely if you use react-redux's connect function while exporting the unconnected component. Instead of importing the default export, you can import the unconnected component within your test...
Example component:
import * as React from "react";
import { connect } from "react-redux";
export const Example = ({ accounts, isLoading }) => { ... };
const mapStateToProps = state => ({ ... });
const mapDispatchToProps = { ... };
export default connect(mapStateToProps, mapDispatchToProps)(Example);
Example test:
import * as React from "react";
import { mount } from "enzyme";
import { Example } from "../index";
const initProps = {
accounts: [],
isLoading: true
};
const fakeAccountData = [{...}, {...}, {...}];
describe("Unconnected Example Component", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(<Example {...initProps } />);
});
it("initially shows a loading indicator", () => {
expect(wrapper.find(".loading-indicator")).exists().toBeTruthy();
});
it("displays the accounts when data is present", () => {
wrapper.setProps({ accounts: fakeAccountData, isLoading: false });
wrapper.update();
expect(wrapper.find(".loading-indicator")).exists().toBeFalsy();
expect(wrapper.find(".accounts").exists()).toBeTruthy();
});
});
I figured out that my test was working incorrectly due to my selector function in my component being implement incorrectly. So the test was actually working properly!
Note: My team is currently using mocked data(waiting for the API team to finish up endpoints).
Originally the useSelector function in my component(that I was testing) looked like:
const { accounts, isLoading } = useSelector(
(state: RootState) => state.accounts,
shallowEqual
)
When I updated this to:
const { accounts, isAccountsLoading } = useSelector(
(state: RootState) => ({
accounts: state.accounts.accounts,
isAccountsLoading: state.accounts.isLoading
}),
shallowEqual
)
My tests worked - here are my final tests:
describe('App', () => {
let rootState = {
...initialState
}
const mockStore = configureStore()
const store = mockStore({ ...rootState })
jest.spyOn(Redux, 'useDispatch').mockImplementation(() => jest.fn())
jest
.spyOn(Redux, 'useSelector')
.mockImplementation((state) => state(store.getState()))
afterEach(() => {
jest.clearAllMocks()
// Resetting State
rootState = {
...initialState
}
})
it('renders correctly', () => {
const wrapper = mount(
<TestWrapper>
<Dashboard />
</TestWrapper>
)
expect(wrapper.find('[data-test="app"]').exists()).toBe(true)
expect(wrapper.find(verticalCard).exists()).toBe(false)
expect(wrapper.find(horizontalCard).exists()).toBe(true)
})
it('renders multiple properly', () => {
rootState.info = mockData.info
const wrapper = mount(
<TestWrapper>
<Dashboard />
</TestWrapper>
)
expect(wrapper.find(verticalCard).exists()).toBe(true)
expect(wrapper.find(horizontalCard).exists()).toBe(false)
})
})

React Redux TypeError - is not a function

I am trying to use React and Redux together to manage my app state. I am trying to call a method off of my actions (SprintActions) , but I keep getting the error
TypeError: props.getAllSprints is not a function
Can you please let me know what am I doing wrong here ?
My main component is Sprint component and it imports SprintActions
This is my Sprint component.
import React, { useEffect } from "react";
import { getSprints } from "./sprintActions";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
export const Sprint = (props) => {
useEffect(() => {
props.getAllSprints();
}, []);
return (
<div style={{ zIndex: 1 }}>
{sprints &&
sprints.map((s) => (
<p>{s.id}</p>
))}
</div>
);
};
Sprint.propTypes = {
getAllSprints: PropTypes.func,
};
const mapStateToProps = (state) => ({
sprints: state.sprints,
});
const mapDispatchToProps = (dispatch) => ({
getAllSprints: bindActionCreators(getSprints, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Sprint);
This is my SprintActions
import * as types from "../constants/ActionTypes";
import Axios from "axios";
export const getSprints = () => {
console.log("get sprints");
return (dispatch) => {
Axios.get("http://127.0.0.1:5000/sprints").then((response) => {
const sprintData = response.data;
dispatch(getAllSprints(sprintData));
});
};
};
export const getAllSprints = (data) => ({
type: types.GET_SPRINTS,
data: data,
});
Thank you for your time and patience.
What you are trying to achieve here should be done either by using redux-thunk(to dispatch an api request) or redux-sagas(a little bit more complicated imo).
You can read this guide which is very helpful!
Your problem is that your getSprints action does not return an object with properties (type, ...payload) as described here. If you use your getAllSprints action instead you'll see that it will work.
Also check the docs of redux for bindActionCreators
I hope this helps
In case you have multiple actions and you want to bind them all you can just do as follows
import * as sprintActions from "./sprintActions";
export const Sprint = (props) => {
useEffect(() => {
props.sprintActions.getSprints();
}, []);
...
};
const mapDispatchToProps = (dispatch) => ({
sprintActions: bindActionCreators(sprintActions, dispatch),
});
Otherwise, you will need to manually call dispatch as suggested in other answers:
export const Sprint = (props) => {
useEffect(() => {
props.getAllSprints();
}, []);
...
};
const mapDispatchToProps = (dispatch) => ({
getAllSprints: () => dispatch(getSprints()),
});
bindActionCreators is used for passing dispatch functionality into components that are not aware to your store, because you are anyhow using mapDispatchToProps inside connect there just no need for it (anyhow the way you implement it is bit wrong, welcome to look on https://redux.js.org/api/bindactioncreators for doc).
Just make your mapDispatch as basic as possible:
const mapDispatchToProps = (dispatch) => ({
getAllSprints: () => dispatch(getSprints()),
});
#
Edit
you might add checker to your useEffect that getAllSprint is function (and this line executed after Redux context set), and a [ props[getAllSprint] ] dependency to run the effect again in case not
useEffect(() =>
typeof props[getAllSprint] == 'function' && props.getAllSprint()
, [ props[getAllSprint] ])

Invalid hook call when using useEffect

I get this error when trying to dispatch an action from my react functional component:
Invalid hook call. Hooks can only be called inside of the body of a function component
This React component does not render anything just listen to keypress event on the page
import { useDispatch } from 'react-redux';
const dispatch = useDispatch();
const {
actions: { moveDown }
} = gameSlice;
type Props = {};
export const Keyboard = ({}: Props) => {
useEffect(() => document.addEventListener('keydown', ({ keyCode }) => dispatch(moveDown())));
return <></>;
};
You will need to use the TypeScript typings for functional components, and provide the props as part of the generic typings. And don't forget to import the required modules.
import React, { useEffect } from 'react';
type Props = {};
export const Keyboard: React.FC<Props> = () => {
useEffect(() => document.addEventListener('keydown', ({ keyCode }) => dispatch(moveDown())));
return <></>;
};

mapDispatchToProps props null in jest test

I have just started my first react job (yay!) and am attempting to write unit tests using jest. i am having an issue with mapDispatchToProps in my test.
please note, the test passes, but it is throwing the following error message:
Warning: Failed prop type: The prop testDispatch is marked as required in TestJest, but its value is undefined in TestJest
error seems to be test related to the test as it is not thrown when running the page in the web browser. also, mapStateToProps works fine, included to show it works.
using enzyme/shallow in order not to interact with the <Child/> component.
testJest.js
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import Child from './Child'
export class TestJest extends React.Component {
render() {
return (<div>ABC - <Child /></div>);
}
}
TestJest.propTypes = {
testDispatch: PropTypes.func.isRequired,
testState: PropTypes.arrayOf(PropTypes.string)
};
const mapStateToProps = (state) => {
return {
testState: state.utilization.pets // ['dog','cat']
};
};
const mapDispatchToProps = (dispatch) => {
return {
testDispatch: () => dispatch({'a' : 'abc'})
};
};
export default connect(mapStateToProps, mapDispatchToProps)(TestJest);
testJest-test.js
import React from 'react';
import { TestJest } from '../Components/testJest';
import TestJestSub2 from '../Components/TestJestSub2';
import { shallow } from 'enzyme';
describe('TESTJEST', () => {
it('matches snapshot', () => {
const wrapper = shallow(<TestJest />);
expect(wrapper).toMatchSnapshot();
});
});
You can pass empty function to TestJest as testDispatch prop:
it('matches snapshot', () => {
const wrapper = shallow(<TestJest testDispatch={() => {}} />);
expect(wrapper).toMatchSnapshot();
});
or pass jest mock function:
it('matches snapshot', () => {
const wrapper = shallow(<TestJest testDispatch={jest.fn()} />);
expect(wrapper).toMatchSnapshot();
});
if you want to check if testDispatch was called (expect(wrapper.instance().props.testDispatch).toBeCalled();)

Resources