Unit Test Redux Action - Thunk Undefined - javascript

Wondering if someone can point out what I expect is a stupid mistake.
I have an action for user login.
I'm trying to test this action, I've followed the redux documentation as well as the redux-mock-store documentation however I keep getting an error as follows:
TypeError: Cannot read property 'default' of undefined
4 | import thunkMiddleware from 'redux-thunk'
5 |
> 6 | const middlewares = [thunkMiddleware] // add your middlewares like `redux-thunk`
| ^
7 | const mockStore = configureStore(middlewares)
8 |
9 | describe("userActions", () => {
at Object.thunkMiddleware (actions/user.actions.spec.js:6:22)
My test code is as follows:
import {userActions} from "./user.actions";
import {userConstants} from "../constants/user.constants";
import configureStore from 'redux-mock-store'
import thunkMiddleware from 'redux-thunk'
const middlewares = [thunkMiddleware] // add your middlewares like `redux-thunk`
const mockStore = configureStore(middlewares)
describe("userActions", () => {
describe("login", () => {
it(`should dispatch a ${userConstants.LOGIN_REQUEST}`, () =>{
const store = mockStore({});
return store.dispatch(userActions.login("someuser", "somepassword")).then(() => {
expect(store.getState().loggingIn).toBeTruthy();
});
})
})
});
I've double checked both redux-thunk and redux-mock-store are included in my npm dev dependencies as well as deleteing the node_modules directory and reinstalling them all with npm install.
Can anyone see what's going wrong?
Thanks
EDIT:
It seems i'm doing something fundamentally wrong, I've tried to simplify it almost back to a clean slate to find where the problem is introduced.
Even with this test:
import authentication from "./authentication.reducer";
import { userConstants } from "../constants/user.constants";
describe("authentication reducer", () => {
it("is a passing test", () => {
authentication();
expect("").toEqual("");
});
});
Against this:
function authentication(){
return "test";
}
export default authentication
I'm getting an undefined error:
● authentication reducer › is a passing test
TypeError: Cannot read property 'default' of undefined
6 |
7 | it("is a passing test", () => {
> 8 | authentication();
| ^
9 | expect("").toEqual("");
10 | });
at Object.<anonymous> (reducers/authentication.reducer.spec.js:8:9)

yes, according to that error, seems you have a problem with module dependencies. Take a look at your webpack configuration.
Concerning the redux-mock-store, I suggest you to create a helper for future testing needs:
import configureStore from 'redux-mock-store'
import thunk from 'redux-thunk'
export default function(middlewares = [thunk], data = {}) {
const mockedStore = configureStore(middlewares)
return mockedStore(data)
}
and you will include it in your test cases and use like that:
beforeEach(() => {
store = getMockStore()
})
afterEach(() => {
store.clearActions()
})

If you wont test redux with thunk you can use redux-thunk-tester module for it.
Example:
import React from 'react';
import {createStore, applyMiddleware, combineReducers} from 'redux';
import {asyncThunkWithRequest, reducer} from './example';
import ReduxThunkTester from 'redux-thunk-tester';
import thunk from 'redux-thunk';
const createMockStore = () => {
const reduxThunkTester = new ReduxThunkTester();
const store = createStore(
combineReducers({exampleSimple: reducer}),
applyMiddleware(
reduxThunkTester.createReduxThunkHistoryMiddleware(),
thunk
),
);
return {reduxThunkTester, store};
};
describe('Simple example.', () => {
test('Success request.', async () => {
const {store, reduxThunkTester: {getActionHistoryAsync, getActionHistoryStringifyAsync}} = createMockStore();
store.dispatch(asyncThunkWithRequest());
const actionHistory = await getActionHistoryAsync(); // need to wait async thunk (all inner dispatch)
expect(actionHistory).toEqual([
{type: 'TOGGLE_LOADING', payload: true},
{type: 'SOME_BACKEND_REQUEST', payload: 'success response'},
{type: 'TOGGLE_LOADING', payload: false},
]);
expect(store.getState().exampleSimple).toEqual({
loading: false,
result: 'success response'
});
console.log(await getActionHistoryStringifyAsync({withColor: true}));
});
});

Related

Vue Testing (JEST): shallowMount renders child components and doesn't handle imports

I recently implemented Jest with Vue-test-utils to test the Vue components of a rather large existing project. This is a vue 2 project using babel. After a long time of setup i finally started to write my first tests.
I am currently encountering two major issues :
shallowMount function does not work as expected because child components are rendered
my tests have trouble handling localized imports in the tested components.
Here is my code that test the auth of my app.
login.spec.js
import { shallowMount, createLocalVue } from "#vue/test-utils";
import Vuex from "vuex";
import Login from "../src/views/user/Login.vue";
const localVue = createLocalVue();
localVue.use(Vuex);
describe("Login", () => {
let actions;
let store;
let state;
let wrapper;
beforeEach(() => {
actions = {
signIn: jest.fn(),
};
//mock store auth module
let auth = {
namespaced: true,
state: {},
actions,
};
store = new Vuex.Store({
modules: {
auth,
},
});
wrapper = shallowMount(Login, { store, localVue });
});
test("should dispatch a store action with user credentials when form is submitted", async () => {
// Setup
//Set login data
const email = "test#example.com";
const password = "password123";
//mount component with data
wrapper.setData({ form: { email: email, password: password } });
//Action
const form = wrapper.find("b-form");
await form.trigger("submit.prevent");
//Assertion
expect(actions.signIn).toHaveBeenCalled();
});
});
For my login workflow i use a store subscriber that listen mutations.
subscriber.js
import store from "./index";
import axios from "axios";
store.subscribe((mutation) => {
//do things
}
This subscriber is executed in my project main.js file :
require("./store/subscriber");
When i run my tests this error occurred :
TypeError: Cannot read property 'subscribe' of undefined
2 | import axios from "axios";
3 |
> 4 | store.subscribe((mutation) => {
the wrapper renders child components of my Login Component even if I use shallowMount()
why store is undefined ? it is imported line 1 in subscriber.js file.

Testing mapDispatchToProps calls have fired with ReactTestingLibrary

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).

Testing Vuex Mutations with Jest state does not change

I have a namespaced Vuex module that I am attempting to test using Jest, after making the mutation to my mocked state it does not appear to change.
Here is my addEvents mutation
addEvents: (state, infos) => {
try {
state.events = state.events.concat(infos);
} catch (error) {
console.error("[ERR vuex event set] ", e.message);
}
event.test.js
import { createLocalVue } from '#vue/test-utils';
import Vuex from 'vuex';
//store
import event from '../index.js'
const localVue = createLocalVue();
localVue.use(Vuex);
describe('event', () => {
//intial state
const state = {
events: []
}
const newEvent = {
text: 'Hello',
eventCode: 'importantEvent'
}
//mutate
event.commit('event/addEvents', newEvent);
expect(state.events).toContainEqual(newEvent)
})
})
I am importing from my store index which has many modules in it. I think I am referencing my desired store properly. When I tried to import the specific module I wanted to test my test was unable to find my mutations.
My jest output returns
FAIL vue/vuex/modules/event.test.js
● event › encountered a declaration exception
expect(received).toContainEqual(expected) // deep equality
Expected value: {"eventCode": "importantEvent", "text": "Hello"}
Received array: []
20 | //mutate
21 | event.commit('event/addEvents', newEvent);
> 22 | expect(state.events).toContainEqual(newEvent)
| ^
23 | })
at Suite.<anonymous> (vue/vuex/modules/event.test.js:22:24)
at Object.<anonymous> (vue/vuex/modules/event.test.js:9:1)
Thanks to Estus Flask for helping me figure out my issue was not referencing store properly. This is the code I have that is working including my reset route which already existed. Renamed my event import to store for clarity.
import { createLocalVue } from '#vue/test-utils';
import Vuex from 'vuex';
//store
import store from '../index.js'
const localVue = createLocalVue();
localVue.use(Vuex);
describe('event mutations', () => {
beforeEach(() => {
//resets state beforeEach
store.commit("event/reset");
});
it('adding events works', () => {
const newEvent = {
text: 'Hello',
eventCode: 'importantEvent'
};
//mutate
store.commit('event/addEvents', newEvent);
expect(store.state.event.events).toContainEqual(newEvent);
});
it('store resets properly between tests and reset works', () => {
expect(store.state.event.events).toHaveLength(0);
});
})

Mocking dayjs with Jest

I'm new to test driven development, I'm trying to implement my simple testing but jest always finds my dayjs as not a function. Also i'm not using typescrcipt so my problem would be similar to this.
I can't seem to find how to setup my jest environment to recognise it as not default export.
Here's my simple test with failed mocking?:
import React from "react";
import { shallow } from "enzyme";
import MainDashboardApp from "../MainDashboard";
jest.mock("dayjs", () => {
return () => jest.requireActual("dayjs")("2020-01-01T00:00:00.000Z");
});
describe("Inititate main dashboard", () => {
it("renders without crashing", () => {
┊ shallow(<MainDashboardApp />);
});
});
Error:
FAIL src/app/main/apps/dashboards/main/__tests__/dashboard.test.js
● Test suite failed to run
TypeError: dayjs is not a function
9 | import { tableData } from "../helpers";
10 |
> 11 | const defaultStart = dayjs().startOf("month").format("YYYY-MM-DD");
| ^
12 | const defaultEnd = dayjs().endOf("month").format("YYYY-MM-DD");
13 |
14 | function IncidentList({
at Object.<anonymous> (src/app/main/apps/operations/incidents/IncidentList.js:11:22)
at Object.<anonymous> (src/app/main/apps/index.js:1:1)
at Object.<anonymous> (src/app/main/apps/dashboards/main/MainDashboard.js:22:1)
at Object.<anonymous> (src/app/main/apps/dashboards/main/__tests__/dashboard.test.js:3:1)
Failed component (which actually renders in chrome):
import React, { useEffect, useCallback, useState } from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import * as incidents from "app/store/ducks/incident.duck";
import MaterialTable, { MTableToolbar } from "material-table";
import { useHistory } from "react-router-dom";
import * as dayjs from "dayjs";
import { DatePicker } from "#material-ui/pickers";
import { tableData } from "../helpers";
const defaultStart = dayjs().startOf("month").format("YYYY-MM-DD");
const defaultEnd = dayjs().endOf("month").format("YYYY-MM-DD");
function IncidentList({
incidentsList,
requestIncidents,
selectedLoc,
startDate,
endDate,
}) {
const history = useHistory();
const [selectedEndDate, handleEndDateChange] = useState(defaultEnd);
const [selectedStartDate, handleStartDateChange] = useState(defaultStart);
// Deps are ok because history constantly updates, and this handleclick does not need
// to be updated as well
const handleClick = useCallback((event, rowData) => {
history.push({
pathname: `/operation/incident-details/${rowData.id}`,
state: { detail: "testing" },
});
}, []);
useEffect(() => {
if (startDate && endDate) {
handleEndDateChange(endDate);
handleStartDateChange(startDate);
}
}, [startDate, endDate]);
MockDate works great for this, no intense mocking code required.
https://www.npmjs.com/package/mockdate
import MockDate from 'mockdate'
import dayjs from 'dayjs'
MockDate.set('2020-01-01')
console.log(dayjs.format())
// >>> 2019-12-31T18:00:00-06:00
Remember to clear the mock after your test with:
MockDate.reset();
Also for your situation you probably want to use DayJS UTC to avoid unexpected date math results
https://day.js.org/docs/en/plugin/utc

How to mock dispatch with jest.fn()

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.

Categories

Resources