How to mock dispatch with jest.fn() - javascript

For example, I want to know what has been dispatched and the argument. The action creator is asynchronous, but I don't care about its implementation, I just want to know if the component dispatches the correct action creator with the correct argument. I've tried this approach:
store.dispatch = jest.fn()
But I can't get any useful information:
I've tried to solve the problem by this way:
expect(store.dispatch.mock.calls[0].toString()).toBe(requestArticles().toString())
But I don't know the argument and I'm sure, that there are a better way to do this. Also of note, I'm using react-testing-library, so I can't use wrapper.instance().props from Enzyme.

Here's an example that worked for me (2022):
(Navbar.test.js)
import store from "../../store/redux-store";
import { useDispatch, useSelector } from "react-redux";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import { render, screen, cleanup, waitFor } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
import Navbar from "../navbar/Navbar";
describe("Navbar", () => {
beforeEach(() => {
// ! WE MAKE SURE THE MOCKS ARE CLEARED BEFORE EACH TEST CASE
useSelectorMock.mockClear();
useDispatchMock.mockClear();
});
afterAll(() => {
cleanup();
});
// ! SETUP THE SPY ON USESELECTOR / USE DISPATCH
// ! WE DO THIS TO BE ABLE TO CHECK IF THE DISPATCH MOCK GOT CALLED AND HOW MANY TIMES
const reactRedux = { useDispatch, useSelector }
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
[...]
test("cliking the sign-out button should sign out user", async () => {
const mockStore = configureStore();
const initialState = {
auth: {
isAuthModalOpen: false,
user: { id: 1, email: "test#test.com" },
notification: null,
isLoggedIn: true,
token: "ABC123",
},
};
let updatedStore = mockStore(initialState);
const mockDispatch = jest.fn();
useDispatchMock.mockReturnValue(mockDispatch);
updatedStore.dispatch = mockDispatch;
// ? HERE THE INITIAL CONTENT OF THE MOCK
// console.log(updatedStore.dispatch.mock);
render(<Provider store={updatedStore}><Navbar /></Provider>);
const signOutBtn = screen.getByTestId("button-sign-out");
expect(signOutBtn).toBeInTheDocument();
expect(updatedStore.dispatch).not.toHaveBeenCalled();
userEvent.click(signOutBtn);
// ? HERE THE CONTENT OF THE MOCK CHANGED
// console.log(updatedStore.dispatch.mock);
expect(updatedStore.dispatch).toHaveBeenCalledTimes(1);
expect(updatedStore.dispatch.mock.lastCall[0].type).toMatch("destroySession");
screen.debug()
console.log(updatedStore.getActions());
});
Notice the difference in the way of importing "useDispatch" from react-redux.
Also, notice the way "mockDispatch" is attached to "updatedStore.dispatch".
Those 2 changes made it work for me.

You can mock the action file and check if the action has been called.
e.g. Lets say foo.action.js is the file which has the action being dispatched.
in the start of your test file before importing the component, you can mock the file as:
const yourActionMock = jest.fn();
jest.mock('<Path to the action file>/foo.action.js', () => ({
yourAction: yourActionMock
}));
now you can test the action called as:
expect(yourActionMock).toHaveBeenCalledWith(<args>)

if you're using react-redux hooks you can do it like so:
import * as reactRedux from 'react-redux';
const mockDispatch = jest.fn();
const mockUseDispatch = jest.spyOn(reactRedux, 'useDispatch');
and then make assertions on mockDispatch as usual.

Related

I got products in console in redux actions but When i use same funtion in component and try to get products by useselector hook i got undefined

This is an API call and in console, i get all products . But when I use the same getProducts function in components I got undefined in console
export const getProducts = ()=> async(dispatch)=>{
try {
const data = await fetch("http://localhost:80/api/products/getallproducts",{
method:"GET",
headers:{
"Content-Type":"application/json"
}
});
const res = await data.json();
console.log(res);
dispatch({type:"SUCCESS_GET_PRODUCTS",payload:res});
} catch (error) {
dispatch({type:"FAIL_GET_PRODUCTS",payload:error.response});
}
}
I use it on Home page and got undefined instead of products as i am using same function of getProducts
import React, { useEffect } from 'react'
import Categories from '../components/Categories'
import Banner1 from '../components/Banner1'
import MaterialUiaresoul from '../components/MaterialUiaresoul'
import ProductSlide from '../components/ProductSlide'
import FeaturedProducts from '../components/FeaturedProducts'
import { useDispatch, useSelector } from 'react-redux'
import { getProducts } from '../redux/actions/action'
const Home = () => {
const products = useSelector(state => state.getproductsdata);
console.log(products)
const dispatch = useDispatch();
useEffect(() => {
dispatch(getProducts());
}, [dispatch]);
return (
<>
<MaterialUiaresoul/>
<ProductSlide/>
<Banner1/>
<Categories/>
<FeaturedProducts />
</>
)
}
export default Home
You are trying to dispatch something that is not redux action.
Let's see, you are trying to call this line dispatch(getProducts());
After getProduct call, it will return a new async function, that doesn't called and expect dispatch to be passed in it.
Normally actions look like this:
export function addTodo(text) {
return { type: ADD_TODO, text }
}
Its just a function that return a plain object with type as a required property.
When dealing with api calls using redux, its better to look into some libraries that will help you, such as redux-thunk or redux-saga for example. Redux actions sync by default and async behavior can be reached with use of some middlewares.
In your example, you can make your code work as expected if you will run your getProduct function, and then run response from it with dispatch passed as first argument:
const dispatch = useDispatch();
const createApiCall = getProduct();
createApiCall(dispatch)
I'm still not sure whether it will work and recommend you to look at redux-thunk. Its pretty easy to learn and use.

Using Redux Toolkit, how do I access the store from a non-react file?

What am I trying to do?
Using Redux Toolkit, I'm trying to access the "store" for a value, specifically "username" which I've created a "slice" for, from a non-React file called SomeFile.js.
What is the code that currently tries to do that?
// userMetadataSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
username: "",
};
const userMetadataSlice = createSlice({
name: "userMetadata",
initialState,
reducers: {
updateUsername: (state, action) => {
const username = action.payload;
state.username = username;
},
},
});
export const { updateUsername } = userMetadataSlice.actions;
export default userMetadataSlice.reducer;
export const selectUsername = (state) => {
return state.userMetadata.username;
}
// SomeFile.js
import { selectUsername } from "../redux/userMetadataSlice";
import { useSelector } from "react-redux";
export const displayUsername = () => {
const username = useSelector(selectUsername);
console.log("Username:", username); // Error.
}
What do I expect the result to be?
To be able to pull the username from the "store".
What is the actual result?
When I try to access the value via "useSelector" from the non-react file an error occurs: React Hook "useSelector" is called in function "selectUsername" which is neither a React function component or a custom React Hook function
What I think the problem could be?
SomeFile.js does not have anything React related within it because it just pulls data from the store and outputs the data.
A solution I've tried that worked was to do this:
// userMetadataSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
username: "",
};
const userMetadataSlice = createSlice({
name: "userMetadata",
initialState,
reducers: {
updateUsername: (state, action) => {
const username = action.payload;
state.username = username;
},
},
});
export const { updateUsername } = userMetadataSlice.actions;
export default userMetadataSlice.reducer;
export const selectUsername = (state) => {
return state.userMetadata.username;
}
// New code here!
export function SelectUsername() {
const username = useSelector(selectUsername);
return username;
}
// SomeFile.js
import { SelectUsername } from "../redux/userMetadataSlice";
export const displayUsername = () => {
console.log("Username:", SelectUsername); // No errors, shows correct output.
}
The solutions I'm looking for is this:
Is my proposed solution the "proper" way to receive info from the "store" in non-React files?
Is there a custom hook solution for this?
Is my proposed solution the "proper" way to receive info from the
"store" in non-React files?
No, it's abusing the Rules of Hooks and React functions. You are directly invoking the SelectUsername React function.
Is there a custom hook solution for this?
No, React hooks work only in React functions and custom React hooks.
You can access your state from your Redux store object.
Store
From your created store object you'll have a getState method to invoke.
getState()​
Returns the current state tree of your application. It is equal to the
last value returned by the store's reducer.
Returns​
(any): The current state tree of your application.
You can export your created store object for import into non-React JS files and they can invoke the getStore method.
import store from '../path/to/store';
...
const state = store.getState();
The useSelector React hook from react-redux won't work outside a React component, but the selectUsername state selector function will.
// SomeFile.js
import store from '../path/to/store';
import { selectUsername } from "../redux/userMetadataSlice";
...
export const displayUsername = () => {
const state = store.getState();
const username = selectUsername(state);
console.log("Username:", username);
return username;
};
See the other Store Methods for subscribing to state changes and dispatching actions to your store from outside React.

How to reset redux store after each test using #testing-library/react in Next.js?

I'm trying to get #testing-framework/react integrated into my Next.js workflow. To do that, I created a test-utils.js file as suggested in the documentation, where I re-export the render method with all my providers:
import React from 'react';
import { render } from '#testing-library/react';
import { ChakraProvider, ColorModeProvider } from '#chakra-ui/react';
import { Provider as ReduxStore } from 'react-redux';
import { useStore } from './lib/init-store';
import theme from './lib/theme';
const providersWrapper = ({ children }) => {
// As pageProps.initialReduxState is undefined in _app.js
const store = useStore(undefined);
return (
<ReduxStore store={store}>
<ChakraProvider resetCSS theme={theme}>
<ColorModeProvider
options={{
useSystemColorMode: true,
}}>
{children}
</ColorModeProvider>
</ChakraProvider>
</ReduxStore>
);
};
const customRender = (ui, options) =>
render(ui, { wrapper: providersWrapper, ...options });
// re-export everything
export * from '#testing-library/react';
// override render method
export { customRender as render };
On the other hand, I have a Counter component and its tests:
import React from 'react';
import { render, fireEvent, act, cleanup } from '../../test-utils';
import Counter from './index';
describe('Counter works properly', () => {
test('it should increment count when +1 button is clicked', async () => {
await act(async () => {
const { findByText } = render(<Counter />);
const initialCount = await findByText('0');
expect(initialCount).toBeInTheDocument();
const incrementButton = await findByText('+');
fireEvent.click(incrementButton);
const incrementedCount = await findByText('1');
expect(incrementedCount).toBeInTheDocument();
cleanup();
});
});
test('it should decrement count when -1 button is clicked', async () => {
await act(async () => {
const { findByText } = render(<Counter />);
const initialCount = await findByText('0');
expect(initialCount).toBeInTheDocument();
const decrementButton = await findByText('-');
fireEvent.click(decrementButton);
const decrementedCount = await findByText('-1');
expect(decrementedCount).toBeInTheDocument();
cleanup();
});
});
});
The jest setup works perfectly, but the second test, can't find a 0 text, because the state of the first test persists. I've confirmed this by swapping them resulting in the same: The first test passes, but the second fails.
This is weird, since the Testing Library documentation explicitly says that cleanup is called after each test automatically and by default (https://testing-library.com/docs/react-testing-library/setup/?fbclid=IwAR0CgDKrHalIhEUAEuP5S355uVYkTScMBATSIMgMPFcOz4ntsNCqgRA3Jyc#skipping-auto-cleanup).
So I'm a little bit lost here, I even tried to pull the celanup function out of render in both tests, and adding a cleanup() at the end of each test, having no different result.
Because of that, I thing that testing library is not the problem, instead, the redux store is not resetting after each test, causing the second test to read the state of the first. But I can't 100% prove it, and even if I could, I don't know how i'd reset it manually, since cleanup is supposed to happen automatically.
If you are curious, you can read the code of the whole project in this repo: https://github.com/AmetAlvirde/testing-library-redux-next-question/
I really hope you can help me, since being unable to test my code is something I really don't want to live with.
Thank you so much in advance.

Dispatch is not a function - React Testing Library with React Redux Hooks

I want to test if the component is dispatching an action to the store, for this, i have to mock the functionalities of React Hooks.
I'm using the useDispatch hook to get the dispatcher function and put inside a variable so them i can call it.
When i run my test suits, even if the useDispatch function is mocked, it returns an error saying that it's not a function.
jest.mock('react-redux', () => ({
useSelector: jest.fn(),
useDispatch: jest.fn(() => {}),
}));
it('should be able open modal', () => {
const { getByTestId } = render(<Dropdown />);
fireEvent.click(getByTestId('dropdown'));
fireEvent.click(getByTestId('button-Transactions'));
const dispatch = jest.fn();
useDispatch.mockReturnValue(dispatch);
expect(dispatch).toHaveBeenCalledWith(openModal('transactions'));
});
The error:
TypeError: dispatch is not a function
19 |
20 | if (item.action) {
> 21 | dispatch(item.action);
| ^
22 | }
23 |
24 | return item;
My component:
const history = useHistory();
const dispatch = useDispatch();
function handleNavigation(item) {
if (item.path) return history.push(item.path);
if (item.action) {
dispatch(item.action);
}
return item;
}
The component was trying to execute a function that wasn't declared yet.
We need to mock before of the render method
const dispatch = jest.fn();
useDispatch.mockReturnValue(jest.fn());
const dropdown = render(<Dropdown />);
It is because when you mocked it, you haven't specified a value.
I thought of two ways to do this:
Having a real store in your test, so you can test the integration:
const mockedStore = { /* the things you need in store in your test */ };
const store = configureStore(mockedStore, browserHistory);
const { getByTestId } = render(<Provider store={store}><Dropdown /></Provider>);
Mock your dispatch, but you ll end up having infinite issues with useSelector (especially if you have more than one useSelector in the tree you rendered).
import * as ReactRedux from 'react-redux';
// useDispatch returns a function which we are mocking here
const mockDispatch = jest.fn();
beforeAll(() => {
ReactRedux.useDispatch = jest.fn().mockImplementation(() => mockDispatch);
});
beforeEach(() => {
ReactRedux.useDispatch.mockClear();
});
expect(mockDispatch).toHaveBeenCalledWith(yourAction);
Please note that the real issue you will face is not with dispatch, but with useSelector.
If you mock it, it will return the value you want. But what if you have more than one in your tree ? for that, I real store is necessary as far as I know.

"Cannot read property 'type' of undefined" in redux store for action defined in external package

As part of my ongoing project to learn React (I'm natively an ASP.NET guy) I've hit this issue. I have a suite of React apps in which I want to use some common UI elements, so I've attempted to break these out into a separate npm package. For the shared components themselves this has worked fine.
However, some of these components depend on redux actions to operate, so I've tried to bundle these actions and a reducer function into the external package. Here's a simplified version of my actions\index.js:
export const SNACKBAR_MESSAGE = "SNACKBAR_MESSAGE";
export const SNACKBAR_HIDE = "SNACKBAR_HIDE";
export function showSnackBarMessage(message) {
console.log('hit 1');
return (dispatch, getState) => {
console.log('hit 2');
dispatch(hideSnackBar());
dispatch({
type: SNACKBAR_MESSAGE,
message: message
});
}
}
export const hideSnackBar = () => {
type: SNACKBAR_HIDE
};
And this is reducer\index.js:
import {
SNACKBAR_MESSAGE,
SNACKBAR_HIDE
} from "../actions";
const initialState = {
snackBarMessage: null,
snackBarVisible: false
};
export default function UiReducer(state = initialState, action) {
switch(action.type) {
case SNACKBAR_MESSAGE:
return Object.assign({}, state, {
snackBarMessage: action.message,
snackBarVisible: true
});
case SNACKBAR_HIDE:
return Object.assign({}, state, {
snackBarMessages: '',
snackBarVisible: false
});
default:
return state;
}
}
This is the same code that worked fine when part of the original project. These are exported by my package's entry point file like this:
// Reducer
export { default as uiReducer } from './reducer';
// Actions
export { showSnackBarMessage as uiShowPrompt } from './actions';
export { hideSnackBar as uiHidePrompt } from './actions';
Then in my consuming project, my default reducer looks like this:
import { routerReducer } from 'react-router-redux';
import { combineReducers } from 'redux';
import { uiReducer } from 'my-custom-ui-package';
// Import local reducers
const reducer = combineReducers(
{
// Some local reducers
ui: uiReducer
}
);
export default reducer;
The problem is when I try to dispatch one of these actions imported from my external package. I include the action, e.g. import { uiShowPrompt } from "my-custom-ui-package"; and dispatch it like dispatch(uiShowPrompt("Show me snackbar")); then I see the two console messages (hit 1 and hit 2) displayed, but then the following error:
Uncaught TypeError: Cannot read property 'type' of undefined
at store.js:12
at dispatch (applyMiddleware.js:35)
at my-custom-ui-package.js:1
at index.js:8
at middleware.js:22
at store.js:15
at dispatch (applyMiddleware.js:35)
at auth.js:28
at index.js:8
at middleware.js:22
The store itself looks like this:
import { createStore, combineReducers, applyMiddleware, compose } from "redux";
import thunk from 'redux-thunk';
import { browserHistory } from "react-router";
import {
syncHistoryWithStore,
routerReducer,
routerMiddleware
} from "react-router-redux";
import reducer from "./reducer";
const loggerMiddleware = store => next => action => {
console.log("Action type:", action.type);
console.log("Action payload:", action.payload);
console.log("State before:", store.getState());
next(action);
console.log("State after:", store.getState());
};
const initialState = {};
const createStoreWithMiddleware = compose(
applyMiddleware(
loggerMiddleware,
routerMiddleware(browserHistory),
thunk)
)(createStore);
const store = createStoreWithMiddleware(reducer, initialState);
export default store;
I'm afraid I don't understand this error. I don't see what I'm doing differently other than essentially moving identical code from my local project to an npm package. Since neither the actions nor reducer actually depend on redux, my npm package doesn't itself have a dependency on react-redux. Is that a problem? If there's anything else I could share to help you help me just let me know. Like I say, I'm still fairly new to all this so clearly there's something I'm not getting right!
The problem might be in declaration of hideSnackBar function
export const hideSnackBar = () => {
type: SNACKBAR_HIDE
};
Here the function is trying to return an Object Literal from Arrow Function. This will always return undefined. As the parser doesn't interpret the two braces as an object literal, but as a block statement. Thus the error, Cannot read property 'type' of undefined as store is expecting an action with property type.
Replace code like this and see if it works.
export const hideSnackBar = () => ({
type: SNACKBAR_HIDE
});
The parentheses forces it to parse as Object Literal. Hope this helps
I had exported it like
export default userReducer();
and not like this:
export default userReducer;
Just get rid of that ()
Found out that it was case of wrong order in receiving the arguments when using redux-thunk.
// wrong argument order
const anAction = () => (getState, dispatch) => {...}
// correct one
const anAction = () => (dispatch, getState) => {...}

Categories

Resources