Currently I have this working test:
import React from 'react';
import { render } from '#testing-library/react';
import AuditList from '../../../components/Audit';
jest.mock('#apollo/client', () => ({
useLazyQuery: jest.fn().mockReturnValue([
jest.fn(),
{
data: {
someTestingData: 'some Value'
},
loading: false,
error: false
}
])
}));
describe('Audit', () => {
it('should render audit', () => {
const rendered = render(<AuditList />);
const getBytext = rendered.queryByText('my title');
expect(getBytext).toBeTruthy();
});
});
But, I want to test different cases: when ´loading/error´ is true/false.
How can I make specific definitions of the mock for different ´it´?
EDIT: I tried this:
import React from 'react';
import { useLazyQuery } from '#apollo/client';
import { render } from '#testing-library/react';
import AuditList from '../../../components/communications/Audit';
jest.mock('#apollo/client');
describe('Audit', () => {
it('should render audit', () => {
useLazyQuery.mockImplementation(() => [
jest.fn(),
{
data: {
someTestingData: 'some Value'
},
loading: false,
error: false
}
]);
const rendered = render(<AuditList />);
const getBytext = rendered.queryByText('Auditoría');
expect(getBytext).toBeTruthy();
});
});
But I get ´ Cannot read property 'mockImplementation' of undefined ´
I want to write unit test which checks if data (onLoad) from dispatching async thunk is delivered into state.
It's first time when i'm writing unit tests and it's black magic for me.
My solution it's not working because my state is always empty object.
My component has following logic:
useEffect(() => {
dispatch(getProducts({ filters: false }));
}, [dispatch, filters]);
Here is what i've tried:
import {
render as rtlRender,
screen,
fireEvent,
cleanup,
} from "#testing-library/react";
import { Provider } from "react-redux";
import { store as myStore } from "store/root-reducer";
import ProductList from "components/product/product-list/product-list";
import { productSlice } from "store/slices/product/product.slice";
describe("Product components", () => {
const renderWithRedux = (component) =>
rtlRender(<Provider store={myStore}>{component}</Provider>);
const thunk =
({ dispatch, getState }) =>
(next) =>
(action) => {
if (typeof action === "function") {
return action(dispatch, getState);
}
return next(action);
};
const create = () => {
const store = {
getState: jest.fn(() => ({})),
dispatch: jest.fn(),
};
const next = jest.fn();
const invoke = (action) => thunk(store)(next)(action);
return { store, next, invoke };
};
const initialState = {
product: null,
products: [],
errorMessage: "",
isFetching: false,
filters: false,
};
it("should render product list after dispatching", async () => {
renderWithRedux(<ProductList />);
const { store, invoke } = create();
invoke((dispatch, getState) => {
dispatch("getProducts"); // i want to dispatch asyncthunk which is called getProducts()
getState();
});
expect(store.dispatch).toHaveBeenCalledWith("getProducts");
expect(store.getState).toHaveBeenCalled();
});
});
Can you include your error message?
I think you're almost there. Since you're rendering an actual component and redux store, you're actually doing an integration-style test. My guess is that your dispatch to getProducts() is firing correctly, but the state isn't updating in your test because you haven't mocked an API response. See how msw is being used to do this in the Redux Testing doc
hope someone can point me the right direction with this. Basically I've created a react app which makes use of hooks, specifically useContext, useEffect and useReducer. My problem is that I can't seem to get tests to detect click or dispatch events of the related component.
Stripped down version of my app can be found at : https://github.com/atlantisstorm/hooks-testing
Tests relate to layout.test.js script.
I've tried various approaches, different ways of mocking dispatch, useContext, etc but no joy with it. Most recent version.
layout.test.js
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import Layout from './layout';
import App from './app';
import { Provider, initialState } from './context';
const dispatch = jest.fn();
const spy = jest
.spyOn(React, 'useContext')
.mockImplementation(() => ({
state: initialState,
dispatch: dispatch
}));
describe('Layout component', () => {
it('starts with a count of 0', () => {
const { getByTestId } = render(
<App>
<Provider>
<Layout />
</Provider>
</App>
);
expect(dispatch).toHaveBeenCalledTimes(1);
const refreshButton = getByTestId('fetch-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(3);
});
});
layout.jsx
import React, { useContext, useEffect } from 'react';
import { Context } from "./context";
const Layout = () => {
const { state, dispatch } = useContext(Context);
const { displayThings, things } = state;
const onClickDisplay = (event) => {
// eslint-disable-next-line
event.preventDefault;
dispatch({ type: "DISPLAY_THINGS" });
};
useEffect(() => {
dispatch({ type: "FETCH_THINGS" });
}, [displayThings]);
const btnText = displayThings ? "hide things" : "display things";
return (
<div>
<button data-testid="fetch-button" onClick={onClickDisplay}>{btnText}</button>
{ displayThings ?
<p>We got some things!</p>
:
<p>No things to show!</p>
}
{ displayThings && things.map((thing) =>
<p>{ thing }</p>
)}
</div>
)
}
export default Layout;
app.jsx
import React from 'react';
import Provider from "./context";
import Layout from './layout';
const App = () => {
return (
<Provider>
<Layout />
</Provider>
)
}
export default App;
context.jsx
import React, { createContext, useReducer } from "react";
import { reducer } from "./reducer";
export const Context = createContext();
export const initialState = {
displayThings: false,
things: []
};
export const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
);
};
export default Provider;
reducer.jsx
export const reducer = (state, action) => {
switch (action.type) {
case "DISPLAY_THINGS": {
const displayThings = state.displayThings ? false : true;
return { ...state, displayThings };
}
case "FETCH_THINGS": {
const things = state.displayThings ? [
"thing one",
"thing two"
] : [];
return { ...state, things };
}
default: {
return state;
}
}
};
I'm sure the answer will be easy when I see it, but just trying to figure out I can detect the click event plus detect the 'dispatch' events? (I've already got separate test in the main app to properly test dispatch response/actions)
Thank you in advance.
EDIT
Ok, I think I've got a reasonable, though not perfect, solution. First I just added optional testDispatch and testState props to the context.jsx module.
new context.jsx
import React, { createContext, useReducer } from "react";
import { reducer } from "./reducer";
export const Context = createContext();
export const initialState = {
displayThings: false,
things: []
};
export const Provider = ({ children, testDispatch, testState }) => {
const [iState, iDispatch] = useReducer(reducer, initialState);
const dispatch = testDispatch ? testDispatch : iDispatch;
const state = testState ? testState : iState;
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
);
};
export default Provider;
Then in layout.test.jsx I just simply pass in mocked jest dispatch function plus state as necessary. Also removed the outer App wrapping as that seemed to prevent the props from being passed through.
new layout.test.jsx
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import Layout from './layout';
import { Provider } from './context';
describe('Layout component', () => {
it('starts with a count of 0', () => {
const dispatch = jest.fn();
const state = {
displayThings: false,
things: []
};
const { getByTestId } = render(
<Provider testDispatch={dispatch} testState={state}>
<Layout />
</Provider>
);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: "FETCH_THINGS" });
const refreshButton = getByTestId('fetch-things-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(2);
// Important: The order of the calls should be this, but dispatch is reporting them
// the opposite way around in the this test, i.e. FETCH_THINGS, then DISPLAY_THINGS...
//expect(dispatch).toHaveBeenNthCalledWith(1, { type: "DISPLAY_THINGS" });
//expect(dispatch).toHaveBeenNthCalledWith(2, { type: "FETCH_THINGS" });
// ... so as dispatch behaves correctly outside of testing for the moment I'm just settling for
// knowing that dispatch was at least called twice with the correct parameters.
expect(dispatch).toHaveBeenCalledWith({ type: "DISPLAY_THINGS" });
expect(dispatch).toHaveBeenCalledWith({ type: "FETCH_THINGS" });
});
});
One little caveat though, as noted above, when the 'fetch-things-button' was fired, it reported the dispatch in the wrong order. :/ So I just settled for knowing the correct calls where triggered, but if anyone knows why the call order isn't as expected I would be pleased to know.
https://github.com/atlantisstorm/hooks-testing update to reflect the above if anyone is interested.
A few months back I was also trying to write unit tests for reducer + context for an app. So, here's my solution to test useReducer and useContext.
FeaturesProvider.js
import React, { createContext, useContext, useReducer } from 'react';
import { featuresInitialState, featuresReducer } from '../reducers/featuresReducer';
export const FeatureContext = createContext();
const FeaturesProvider = ({ children }) => {
const [state, dispatch] = useReducer(featuresReducer, featuresInitialState);
return <FeatureContext.Provider value={{ state, dispatch }}>{children}</FeatureContext.Provider>;
};
export const useFeature = () => useContext(FeatureContext);
export default FeaturesProvider;
FeaturesProvider.test.js
import React from 'react';
import { render } from '#testing-library/react';
import { renderHook } from '#testing-library/react-hooks';
import FeaturesProvider, { useFeature, FeatureContext } from './FeaturesProvider';
const state = { features: [] };
const dispatch = jest.fn();
const wrapper = ({ children }) => (
<FeatureContext.Provider value={{ state, dispatch }}>
{children}
</FeatureContext.Provider>
);
const mockUseContext = jest.fn().mockImplementation(() => ({ state, dispatch }));
React.useContext = mockUseContext;
describe('useFeature test', () => {
test('should return present feature toggles with its state and dispatch function', () => {
render(<FeaturesProvider />);
const { result } = renderHook(() => useFeature(), { wrapper });
expect(result.current.state.features.length).toBe(0);
expect(result.current).toEqual({ state, dispatch });
});
});
featuresReducer.js
import ApplicationConfig from '../config/app-config';
import actions from './actions';
export const featuresInitialState = {
features: [],
environments: ApplicationConfig.ENVIRONMENTS,
toastMessage: null
};
const { INITIALIZE_DATA, TOGGLE_FEATURE, ENABLE_OR_DISABLE_TOAST } = actions;
export const featuresReducer = (state, { type, payload }) => {
switch (type) {
case INITIALIZE_DATA:
return {
...state,
[payload.name]: payload.data
};
case TOGGLE_FEATURE:
return {
...state,
features: state.features.map((feature) => (feature.featureToggleName === payload.featureToggleName
? {
...feature,
environmentState:
{ ...feature.environmentState, [payload.environment]: !feature.environmentState[payload.environment] }
}
: feature))
};
case ENABLE_OR_DISABLE_TOAST:
return { ...state, toastMessage: payload.message };
default:
return { ...state };
}
};
featuresReducer.test.js
import { featuresReducer } from './featuresReducer';
import actions from './actions';
const { INITIALIZE_DATA, TOGGLE_FEATURE, ENABLE_OR_DISABLE_TOAST } = actions;
describe('Reducer function test', () => {
test('should initialize data when INITIALIZE_DATA action is dispatched', () => {
const featuresState = {
features: []
};
const action = {
type: INITIALIZE_DATA,
payload: {
name: 'features',
data: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
}
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
});
});
test('should toggle the feature for the given feature and environemt when TOGGLE_FEATURE action is disptched', () => {
const featuresState = {
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}, {
featureId: '23458', featureName: 'WO photo download', featureToggleName: '23458_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
};
const action = {
type: TOGGLE_FEATURE,
payload: { featureToggleName: '23456_WOPhotoDownload', environment: 'sit' }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}, {
featureId: '23458', featureName: 'WO photo download', featureToggleName: '23458_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
});
});
test('should enable the toast message when ENABLE_OR_DISABLE_TOAST action is dispatched with the message as part of payload', () => {
const featuresState = {
toastMessage: null
};
const action = {
type: ENABLE_OR_DISABLE_TOAST,
payload: { message: 'Something went wrong!' }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({ toastMessage: 'Something went wrong!' });
});
test('should disable the toast message when ENABLE_OR_DISABLE_TOAST action is dispatched with message as null as part of payload', () => {
const featuresState = {
toastMessage: null
};
const action = {
type: ENABLE_OR_DISABLE_TOAST,
payload: { message: null }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({ toastMessage: null });
});
test('should return the current state when the action with no specific type is dispatched', () => {
const featuresState = {
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}]
};
const action = {};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}]
});
});
});
I'm trying to test that a function gets called with the correct arguments, but because I'm using useDispatch from react-redux, I'm getting the following error: Actions must be plain objects. Use custom middleware for async actions.
I've wrapped my test component in renderWithRedux like it says in the documentation.
Test Setup:
const renderWithRedux = (
ui,
{ initialState, store = createStore(reducer, initialState) } = {}
) => {
return {
...render(<Provider store={store}>{ui}</Provider>),
store,
};
};
const templateId = faker.random.number();
const setup = () => {
const props = {
history: {
push: jest.fn(),
},
};
viewTemplateUrl.mockImplementationOnce(() => jest.fn(() => () => {}));
templatePostThunk.mockImplementationOnce(
jest.fn(() => () => Promise.resolve(templateId))
);
const {
container,
getByText,
getByLabelText,
rerender,
debug,
} = renderWithRedux(<NewTemplateForm {...props} />);
return {
debug,
templateId,
props,
container,
rerender,
getByText,
getByLabelText,
templateNameTextField: getByLabelText('Template Name'),
templateNameInput: getByLabelText('Template Name Input'),
saveTemplateButton: getByText('Save Template'),
cancelButton: getByText('Cancel'),
};
};
Failing test:
test('save template calls handleSubmit, push, and viewTemplateUrl', async () => {
const { templateNameInput, saveTemplateButton, props } = setup();
fireEvent.change(templateNameInput, { target: { value: 'Good Day' } });
fireEvent.click(saveTemplateButton);
await expect(templatePostThunk).toHaveBeenCalledWith({
name: 'Good Day',
});
expect(props.history.push).toHaveBeenCalled();
expect(viewTemplateUrl).toHaveBeenCalledWith({ templateId });
});
It should be passing.
The answer was to mock useDispatch and wrap the parent component in renderWithRedux instead.
import { render, fireEvent } from '#testing-library/react';
import '#testing-library/react/cleanup-after-each';
import { useDispatch } from 'react-redux';
import faker from 'faker';
import { NewTemplateForm } from './NewTemplateForm';
import { templatePostThunk } from '../../../redux/actions/templatePost';
import { viewTemplateUrl } from '../../../utils/urls';
jest.mock('../../../redux/actions/templatePost');
jest.mock('../../../utils/urls');
jest.mock('react-redux');
describe('<NewTemplateForm/> controller component', () => {
useDispatch.mockImplementation(() => cb => cb());
const setup = () => {
const props = {
history: {
push: jest.fn(),
},
};
const templateId = faker.random.number();
viewTemplateUrl.mockImplementationOnce(() => () => {});
templatePostThunk.mockImplementationOnce(() => async () => ({
data: { id: templateId },
}));
const { container, getByText, getByLabelText, rerender, debug } = render(
<NewTemplateForm {...props} />
);
You're trying to dispatch a thunk action creator:
await expect(templatePostThunk).toHaveBeenCalledWith({
name: 'Good Day',
});
But you're setting up the store without the thunk middleware:
store = createStore(reducer, initialState)
You would need to add the thunk middleware to the createStore() call for this to work right.
nativeusingreact-redux,react-thunk,handleActionswithducks structure` and trying to dispatch action function to change state.
It worked actually until this morning, but it doesn't work anymore.
I have no idea what I changed. Even worse, I didn't commit because this project is for practicing react native, so I cannot undo my work.
If I'm right that I understood correctly, dispatch of connect in container component should call fetchName() in categoryImgListMod.js(action).
However, I guess dispatch never works here.
So state never changes.
If you give me any of advice, it would be very helpful for me, and I would appreciate you.
Here's my code
categoryListContainer.js
import React, {Component} from 'react';
import {View} from 'react-native';
import { connect } from 'react-redux';
import CategoryImgList from '../components/categoryImgList';
import * as CategoryImgActions from '../store/modules/categoryImgListMod';
class CategoryImgListContainer extends Component {
loadNames = async () => {
console.log(this.props);
const { CategoryImgActions } = this.props;
try {
await CategoryImgActions.fetchName();
} catch (e) {
console.log(e);
}
}
render() {
const {container} = styles;
const { loadNames } = this;
return (
<View style={container}>
<CategoryImgList names={loadNames}/>
</View>
);
}
}
const styles = {
container: {
height: '100%'
}
}
export default connect(
({categoryImgListMod}) => ({
name: categoryImgListMod.name
}),
(dispatch) => ({
fetchName: () => {
dispatch(CategoryImgActions.fetchName())
}
})
)(CategoryImgListContainer);
categoryImgListMod.js
import {handleActions} from 'redux-actions';
// firestore
import * as db from '../../shared';
// Action types
const GET_CATEGORY_NAME_PENDING = 'categoryImgList/GET_CATEGORY_NAME_PENDING';
const GET_CATEGORY_NAME_SUCCESS = 'categoryImgList/GET_CATEGORY_NAME_SUCCESS';
const GET_CATEGORY_NAME_FAILURE = 'categoryImgList/GET_CATEGORY_NAME_FAILURE';
// action creator
export const fetchName = () => async (dispatch) => {
dispatch({type: GET_CATEGORY_NAME_PENDING});
try {
const response = await db.getCategoryNames();
const arr = [];
response.docs.forEach(res => {
arr.push(res.id);
});
dispatch({type: GET_CATEGORY_NAME_SUCCESS, payload: arr});
return arr;
} catch (e) {
console.log(e);
dispatch({type: GET_CATEGORY_NAME_FAILURE, payload: e});
}
}
const initialState = {
fetching: false,
error: false,
name: []
};
// Reducer
export default handleActions({
[GET_CATEGORY_NAME_PENDING]: (state) => ({ ...state, fetching: true, error: false }),
[GET_CATEGORY_NAME_SUCCESS]: (state, action) => ({ ...state, fetching: false, name: action.payload }),
[GET_CATEGORY_NAME_FAILURE]: (state) => ({ ...state, fetching: false, error: true })
}, initialState);
I solved using bindActionCreators.
But, still I don't understand why it never dispatched.
(dispatch) => ({
CategoryImgActions: bindActionCreators(categoryImgActions, dispatch)
})
I believe its the curly braces which are the problem, try this:
(dispatch) => ({
fetchName: () => dispatch(CategoryImgActions.fetchName())
})
If you are using curly braces you need to explicitly return:
(dispatch) => ({
fetchName: () => {
return dispatch(CategoryImgActions.fetchName());
}
})