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.
Related
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.
I am using mapDispatchToProps to make a dispatch call to an API in the useEffect of my functional component.
I am a little stuck as how to actually test this in my unit tests with React Testing Library.
I can pass my Redux store quite easily, however I don't think I've ever had to pass the dispatch before and I'm a little lost.
I did try to pass the dispatch function with my store, but this of course didn't work.
Component
const mapDispatchToProps = dispatch => ({
myDispatchFunction: () => dispatch(someDispatch())
});
const mapStateToProps = ({someStateProp}) => ({
myStateProp: !!someStateProp // This isn't important
});
const MyComp = ({myDispatchFunction}) => {
useEffect(() => {
!!myStateProp && myDispatchFunction();
}, []);
return ...
}
Test
it('Should trigger dispatch function on load', () => {
const mockFunc = jest.fn(); // My attempt at mocking the dispatch call
const store = {someStateProp: true, myDispatchFunction: mockFunc};
render(
<Provider store={mockStore(store)}>
<MyComponent />
</Provider>
);
expect(mockFunc).toHaveBeenCalled();
});
This fails...
Your code has no real meaning, but the same with the test method, try not to mock functions and modules that have no side effects, I/O operations, and use their original implementation.
For your example, don't mock mapStateToProps, mapDispatchToProps, and dispatch functions. We can create a test store, populate the test state data, and collect the dispatch actions in the component.
If your component uses some state slice, verify that your component is rendering correctly. This is also called behavior testing. This testing strategy is more robust if the component behaves correctly, regardless of whether your implementation changes.
Why not mock? Like mapStateToProps, where a mock implementation changes its behavior if you don't know how it is implemented, resulting in an error-based implementation of the test case. Your tests may pass but the actual code at runtime is not correct.
E.g.
index.tsx:
import { useEffect } from 'react';
import { connect } from 'react-redux';
const mapDispatchToProps = (dispatch) => ({
myDispatchFunction: () => dispatch({ type: 'SOME_ACTION' }),
});
const mapStateToProps = ({ someStateProp }) => ({
myStateProp: !!someStateProp,
});
const MyComp = ({ myDispatchFunction, myStateProp }) => {
useEffect(() => {
!!myStateProp && myDispatchFunction();
}, []);
return null;
};
export default connect(mapStateToProps, mapDispatchToProps)(MyComp);
index.test.tsx:
import { render } from '#testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import MyComp from './';
describe('71555438', () => {
test('should pass', () => {
let dispatchedActions: any[] = [];
const store = createStore(function rootReducer(state = { someStateProp: 'fake value' }, action) {
if (!action.type.startsWith('##redux')) {
dispatchedActions.push(action);
}
return state;
});
render(
<Provider store={store}>
<MyComp />
</Provider>
);
expect(dispatchedActions).toEqual([{ type: 'SOME_ACTION' }]);
});
});
Test result:
PASS stackoverflow/71555438/index.test.tsx (9.562 s)
71555438
✓ should pass (16 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.tsx | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 10.565 s
Note: We should ignore the action dispatched by createStore internally and we only need to collect the actions dispatched by users(our code).
export default function MyQuestions() {
const router = useRouter();
const [auth, setAuth] = useState(false);
const checkAuth = async () => {
const loggedInUsername = await getUsername();
if (router.query.username === loggedInUsername) return setAuth(true);
return;
};
checkAuth();
This is a part of a React component where I execute the checkAuth function. I thought it should execute only once but that is not the case. It is executed 4 times and if I remove the returns it is executed even more than 10 times and I don't understand why. In js a function that reaches the end should stop automatically.
Why does this happen?
In this code router is of Next.js
What are the conditions under which the check should be re-run? This is what useEffect is intended for. useEffect accepts a function to run the desired effect, and a list of dependencies to specify when an effect should be run -
import { useRouter } from ...
import { useEffect, useState } from "react"
function MyQuestions() {
const router = useRouter()
const [auth, setAuth] = useState(false)
useEffect(async () => {
const loggedInUsername = await getUsername()
if (router.query.username === loggedInUsername)
setAuth(true)
}, [getUsername, router.query.username, setAuth])
return <>...</>
}
Any free variable inside the effect must be listed as a dependency of the effect. There's one issue however. setAuth will be a new function each time MyQuestions is rendered. To ensure setAuth will be the same for each render, we can use useCallback -
import { useRouter } from ...
import { useEffect, useCallback, useState } from "react"
function MyQuestions() {
const router = useRouter()
const [auth, setAuth] = useState(false)
const authenticate =
useCallback(_ => setAuth(true), [])
useEffect(async () => {
const loggedInUsername = await getUsername()
if (router.query.username === loggedInUsername)
authenticate()
}, [getUsername, router.query.username, authenticate])
return <>...</>
}
Now the effect will only re-run when getUsername, router.query.username or authenticate changes. Considering getUsername and authenticate are functions and should not change, we can expect that the effect will only re-run when router.query.username changes.
I haven't used nextjs but i suppose it happens because it is executed on every render of the router component.
If you want to use it once, just call it in a use effect when the component mounts.
useEffect(() => {
checkAuth();
}, []) // This will run once, when the component mounts
There is no need to return the setState call:
const checkAuth = async () => {
const loggedInUsername = await getUsername();
if (router.query.username === loggedInUsername) setAuth(true);
};
Also because you are calling the checkAuth() function right after you call it. Setting state in React causes a re-render. So the reason it is executed 4 to 10 times is because your MyQuestion component re-renders when you setState(), and when it rerenders, is hits the checkAuth() function again and the cycle repeats.
As mentioned put the functionality in a useEffect() with an empty dependency array:
useEffect(() => {
const loggedInUsername = await getUsername();
if (router.query.username === loggedInUsername) return setAuth(true);
}, [])
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.
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.