how to write unit test for multiple conditions in redux-thunk? - javascript

I have a thunk like the below
export const goToNewExperience = (request?: sessionRequest): ThunkAction => {
return async (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const isNewExp = state.isNewExp;
if (isNewExp) {
dispatch(updateExperience({
type: UPDATE_EXP,
'NewExperience'
}))
} else if (request && request.isError) {
dispatch(updateExperience({
type: UPDATE_EXP,
'ErrorExperience'
}));
}
};
};
how to test the other action dispatchers in one redux-thunk based on a condition ? any best practices?
I wrote like this but looking for best practices
it('should update exp with New Exp', done => {
const store = createStoreWithState();
const session = {isNewExp:true};
store.dispatch(updateSession(session));
const dispatch = jest.fn();
goToNewExperience()(dispatch, () => store.getState()).then(_ => {
expect((dispatch.mock.calls[0][0]: any).type).toEqual(UPDATE_EXP);
expect((dispatch.mock.calls[0][0]: any).payload).toEqual('NewExperience');
done();
});
});
it('should update exp with Error Exp', done => {
const store = createStoreWithState();
const session = {isNewExp:false};
store.dispatch(updateSession(session));
const dispatch = jest.fn();
goToNewExperience({isError:true})(dispatch, () => store.getState()).then(_ => {
expect((dispatch.mock.calls[0][0]: any).type).toEqual(UPDATE_EXP);
expect((dispatch.mock.calls[0][0]: any).payload).toEqual('ErrorExperience');
done();
});
});

Here is the best practice for write unit testing for actionCreators of redux:
actionCreators.ts:
import { ThunkAction } from 'redux-thunk';
import { Dispatch, AnyAction } from 'redux';
export const UPDATE_EXP = 'UPDATE_EXP';
export const updateExperience = action => ({ type: action.type, payload: { experience: action.experience } });
export interface ISessionRequest {
isError: boolean;
}
type GetState<S = IState> = () => S;
export interface IState {
isNewExp: boolean;
}
export const goToNewExperience = (request?: ISessionRequest): ThunkAction<any, IState, {}, AnyAction> => {
return async (dispatch: Dispatch, getState: GetState<IState>) => {
const state = getState();
const isNewExp = state.isNewExp;
if (isNewExp) {
dispatch(
updateExperience({
type: UPDATE_EXP,
experience: 'NewExperience'
})
);
} else if (request && request.isError) {
dispatch(
updateExperience({
type: UPDATE_EXP,
experience: 'ErrorExperience'
})
);
}
};
};
actionCreators.spec.ts:
import { goToNewExperience, IState, UPDATE_EXP, ISessionRequest } from './actionCreators';
import createMockStore from 'redux-mock-store';
import { AnyAction } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const mockStore = createMockStore<IState, ThunkDispatch<IState, any, AnyAction>>(middlewares);
describe('goToNewExperience', () => {
it('update new experience', () => {
const initialState = { isNewExp: true };
const store = mockStore(initialState);
return store.dispatch(goToNewExperience()).then(() => {
expect(store.getActions()).toEqual([{ type: UPDATE_EXP, payload: { experience: 'NewExperience' } }]);
});
});
it('update error experience', () => {
const initialState = { isNewExp: false };
const store = mockStore(initialState);
const request: ISessionRequest = { isError: true };
return store.dispatch(goToNewExperience(request)).then(() => {
expect(store.getActions()).toEqual([{ type: UPDATE_EXP, payload: { experience: 'ErrorExperience' } }]);
});
});
it('do nothing', () => {
const initialState = { isNewExp: false };
const store = mockStore(initialState);
const request: ISessionRequest = { isError: false };
return store.dispatch(goToNewExperience(request)).then(() => {
expect(store.getActions()).toEqual([]);
});
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/49824394/actionCreators.spec.ts (5.028s)
goToNewExperience
✓ update new experience (10ms)
✓ update error experience (1ms)
✓ do nothing (1ms)
-------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-------------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
actionCreators.ts | 100 | 100 | 100 | 100 | |
-------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 6.09s, estimated 7s
Here is the completed demo: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/49824394

Related

How to test a function that's called inside a useEffect hook

I have a function that is called inside a useEffect and I'm not able to pass coverage to there. The function changes the value of a state depending of the viewport width, for render html. Basically I do a conditional rendering. This is the code of the function updateMedia:
import { useEffect, useState } from "react";
import { Contact } from "../../features/contacts/models/Contact";
import IndividualContactStyled from "./IndividualContactStyled";
interface ContactProps {
contact: Contact;
}
// eslint-disable-next-line #typescript-eslint/no-redeclare
const IndividualContact = ({ contact }: ContactProps): JSX.Element => {
const initialState = false;
const [isDesktop, setIsDesktop] = useState(initialState);
const updateMedia = () => {
setIsDesktop(window.innerWidth > 799);
};
useEffect(() => {
window.addEventListener("resize", updateMedia);
return () => window.removeEventListener("resize", updateMedia);
});
return (
<IndividualContactStyled className="contact">
{isDesktop && <span className="contact__email">{contact.email}</span>}
{isDesktop && (
<span className="contact__phoneNumber">{contact.phoneNumber}</span>
)}
</div>
</IndividualContactStyled>
);
};
export default IndividualContact;
Now, the coverage don't pass for the updateMedia function. I've made this test, if it helps:
import IndividualContact from "./IndividualContact";
import { render, screen, waitFor } from "#testing-library/react";
describe("Given a IndividualContact component", () => {
describe("When it is instantiated with a contact and in a viewport bigger than 800px", () => {
const contact = {
name: "Dan",
surname: "Abramov",
email: "dan#test.com",
phoneNumber: "888555222",
owner: "owner",
};
test("Then it should render the 'email' and the 'phoneNumber' of the contact", async () => {
global.innerWidth = 1000;
global.dispatchEvent(new Event("resize"));
render(<IndividualContact contact={contact} />);
await waitFor(() => {
expect(screen.getByText("dan#test.com")).toBeInTheDocument();
});
});
});
});
If anyone can help me I would be very grateful. Thanks!
You should render the component and register the resize event on the window first. Then change the value of window.innerWidth and dispatch a resize event on the window.
E.g.
index.tsx:
import React, { useEffect, useState } from 'react';
type Contact = any;
interface ContactProps {
contact: Contact;
}
const initialState = false;
const IndividualContact = ({ contact }: ContactProps) => {
const [isDesktop, setIsDesktop] = useState(initialState);
const updateMedia = () => {
setIsDesktop(window.innerWidth > 799);
};
useEffect(() => {
window.addEventListener('resize', updateMedia);
return () => window.removeEventListener('resize', updateMedia);
});
return (
<div>
{isDesktop && <span className="contact__email">{contact.email}</span>}
{isDesktop && <span className="contact__phoneNumber">{contact.phoneNumber}</span>}
</div>
);
};
export default IndividualContact;
index.test.tsx:
import IndividualContact from './';
import { act, fireEvent, render, screen, waitFor } from '#testing-library/react';
import '#testing-library/jest-dom';
import React from 'react';
describe('Given a IndividualContact component', () => {
describe('When it is instantiated with a contact and in a viewport bigger than 800px', () => {
const contact = {
name: 'Dan',
surname: 'Abramov',
email: 'dan#test.com',
phoneNumber: '888555222',
owner: 'owner',
};
test("Then it should render the 'email' and the 'phoneNumber' of the contact", async () => {
render(<IndividualContact contact={contact} />);
global.innerWidth = 1000;
act(() => {
global.dispatchEvent(new Event('resize'));
});
await waitFor(() => {
expect(screen.queryByText('dan#test.com')).toBeInTheDocument();
});
});
});
});
Test result:
PASS stackoverflow/73652164/index.test.tsx (11.685 s)
Given a IndividualContact component
When it is instantiated with a contact and in a viewport bigger than 800px
✓ Then it should render the 'email' and the 'phoneNumber' of the contact (43 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: 12.385 s
package versions:
"jest": "^26.6.3",
"#testing-library/react": "^11.2.7",
"react": "^16.14.0",

How do I mock only one value returned by a custom hook?

I have a simple TodoList component that uses a custom hook useTodos
import { useState } from 'react'
export const useTodos = (initialTodos = []) => {
const [todos, setTodos] = useState(initialTodos)
const addTodo = (value) => {
const updatedTodos = [...todos, value]
setTodos(updatedTodos)
}
const removeTodo = (index) => {
const updatedTodos = todos.filter((todo, i) => i !== index)
setTodos(updatedTodos)
}
return { todos, addTodo, removeTodo }
}
I would like to test the component with React Testing Library.
In order to do so, I want to mock the initial todos returned by the hook.
jest.mock('hooks/useTodos', () => ({
useTodos: () => ({
todos: ['Wake up', 'Go to work'],
}),
}))
But the methods addTodo and removeTodo are then undefined. On the other hand, when I mock them with jest.fn() they do not work anymore.
Is there any way to mock only todos and keep other methods working?
You can create a mocked useTodos hook with mock todos initial state based on the real useTodos hook.
hooks.js:
import { useState } from 'react';
export const useTodos = (initialTodos = []) => {
const [todos, setTodos] = useState(initialTodos);
const addTodo = (value) => {
const updatedTodos = [...todos, value];
setTodos(updatedTodos);
};
const removeTodo = (index) => {
const updatedTodos = todos.filter((todo, i) => i !== index);
setTodos(updatedTodos);
};
return { todos, addTodo, removeTodo };
};
index.jsx:
import React from 'react';
import { useTodos } from './hooks';
export default function MyComponent() {
const { todos, addTodo, removeTodo } = useTodos();
return (
<div>
{todos.map((todo, i) => (
<p key={i}>
{todo}
<button type="button" onClick={() => removeTodo(i)}>
Remove
</button>
</p>
))}
<button type="button" onClick={() => addTodo('have a drink')}>
Add
</button>
</div>
);
}
index.test.jsx:
import React from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import MyComponent from '.';
jest.mock('./hooks', () => {
const { useTodos } = jest.requireActual('./hooks');
return {
useTodos: () => useTodos(['Wake up', 'Go to work']),
};
});
describe('69399677', () => {
test('should render todos', () => {
render(<MyComponent />);
expect(screen.getByText(/Wake up/)).toBeInTheDocument();
expect(screen.getByText(/Go to work/)).toBeInTheDocument();
});
test('should add todo', () => {
render(<MyComponent />);
fireEvent.click(screen.getByText(/Add/));
expect(screen.getByText(/have a drink/)).toBeInTheDocument();
});
test('should remove todo', () => {
render(<MyComponent />);
fireEvent.click(screen.getByText(/Go to work/).querySelector('button'));
expect(screen.queryByText(/Go to work/)).not.toBeInTheDocument();
});
});
test result:
PASS examples/69399677/index.test.jsx (8.788 s)
69399677
✓ should render todos (26 ms)
✓ should add todo (10 ms)
✓ should remove todo (4 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 0 | 100 | 100 |
hooks.js | 100 | 0 | 100 | 100 | 3
index.jsx | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 9.406 s

React Tests fails when i use document.getElementById (Jest+Enzyme)

I am working on a React form and have an onSubmit function to it. I only added below lines in onSubmit function.
const id = this.getErrorPositionById();
const errorPosition = document.getElementById(id).offsetTop; //CANNOT READ PROPERTY 'offsetTop' of null
window.scrollTo({
top: errorPosition,
behavior: "smooth"
});
And this is the onSubmit function.
public getErrorPositionById = () => {
const error = this.state.errors;
return Object.keys(error).find(id => error[id] != null);
};
public onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const id = this.getErrorPositionById();
const errorPosition = document.getElementById(id).offsetTop;
window.scrollTo({
top: errorPosition,
behavior: "smooth"
});
if (
!this.state.isValid ||
(!this.props.allowMultipleSubmits && this.state.isSubmitted)
) {
return;
}
Promise.all(this.validateFields())
.then((validationErrors: IValidationErrors[]) => {
this.setError(Object.assign({}, ...validationErrors), () => {
this.isFormValid() ? this.setSubmitted(e) : this.scrollFormToView();
});
})
.then(() => {
const newErrors = this.state.errors;
this.setState({ errors: { ...newErrors, ...this.props.apiErrors } });
});
};
Here is the test case
beforeEach(() => {
jest.clearAllMocks();
formFields = jest.fn();
onSubmit = jest.fn();
onValidate = jest.fn();
validate = jest.fn();
mockPreventDefault = jest.fn();
mockEvent = jest.fn(() => ({ preventDefault: mockPreventDefault }));
mockValidateAllFields = jest.fn(() => Promise);
mockChildFieldComponent = { validate };
instance = formWrapper.instance();
});
it("should not reValidate if form has been submitted already", () => {
instance.validateFields = mockValidateAllFields;
instance.setSubmitted();
expect(instance.state.isSubmitted).toBe(true);
instance.onSubmit(mockEvent());
expect(mockValidateAllFields).toHaveBeenCalledTimes(0);
});
The test case fails with error
TypeError: Cannot read property 'offsetTop' of null
on below line
const errorPosition = document.getElementById(id).offsetTop;
Can someone please help me understand how to eliminate this error.
You should make a stub for document.getElementById(id). For simple, I remove your business logic from the component.
E.g.
index.tsx:
import React, { Component } from 'react';
class SomeComponent extends Component {
state = {
errors: {
'#selector': {},
},
};
public getErrorPositionById = () => {
const error = this.state.errors;
return Object.keys(error).find((id) => error[id] != null);
};
public onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const id = this.getErrorPositionById() as string;
const errorPosition = document.getElementById(id)!.offsetTop;
window.scrollTo({
top: errorPosition,
behavior: 'smooth',
});
};
public render() {
return (
<div>
<form onSubmit={this.onSubmit}></form>
</div>
);
}
}
export default SomeComponent;
index.spec.tsx:
import React from 'react';
import { shallow } from 'enzyme';
import SomeComponent from './';
describe('SomeComponent', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should handle submit correctly', async () => {
const mElement = { offsetTop: 123 };
document.getElementById = jest.fn().mockReturnValueOnce(mElement);
window.scrollTo = jest.fn();
const wrapper = shallow(<SomeComponent></SomeComponent>);
const mEvent = { preventDefault: jest.fn() };
wrapper.find('form').simulate('submit', mEvent);
expect(mEvent.preventDefault).toHaveBeenCalledTimes(1);
expect(document.getElementById).toBeCalledWith('#selector');
expect(window.scrollTo).toBeCalledWith({ top: 123, behavior: 'smooth' });
});
});
Unit test result with coverage report:
PASS src/stackoverflow/53352420/index.spec.tsx (12.859s)
SomeComponent
✓ should handle submit correctly (20ms)
-----------|----------|----------|----------|----------|-------------------|
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: 14.751s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/53352420

How to test functional component with async callback inside useEffect using snapshots

I'm trying to write unit tests on my component, it looks like this.
export const myComponent = ({text, list, getData = transport.getData}) => {
const [rows, setRows] = React.useState([]);
React.useEffect(() => {
const fetchData = async () => {
const rows = await getData(list);
setRows(rows);
};
fetchData();
}, [list]);
if (rows.length === 0) {
return null;
}
return (
// some JSX
);
};
The problem is that component fetches data via async function, so it will be called after the component check if rows is empty and return null.
if (rows.length === 0) {
return null;
}
I mocked getData so it should return some values. But still, I couldn't understand how I should cover this component with unit testing. I suppose it should be a snapshot, perhaps it is not right.
My test:
import React from 'react';
import {myComponent} from '../components/myComponent';
import renderer from 'react-test-renderer';
describe('myComponent', () => {
test('should renders correctly', async () => {
const mock = {
text: 'text',
list: [],
getData: () =>
Promise.resolve([
{
// ...
},
]),
};
const component = renderer.create(<myComponent text={mock.text}
list={mock.list} getData={mock.getData}/>);
let popup = component.toJSON();
expect(popup).toMatchSnapshot();
});
});
Here is the unit test solution:
index.tsx:
import React from 'react';
const transport = {
async getData(list) {
return [{ id: 1 }];
}
};
export const MyComponent = ({ text, list, getData = transport.getData }) => {
const [rows, setRows] = React.useState<any[]>([]);
React.useEffect(() => {
console.count('useEffect');
const fetchData = async () => {
console.count('fetchData');
const newRows = await getData(list);
setRows(newRows);
};
fetchData();
}, [list]);
if (rows.length === 0) {
return null;
}
return <div>rows count: {rows.length}</div>;
};
index.spec.tsx:
import React from 'react';
import { MyComponent } from './';
import renderer, { act } from 'react-test-renderer';
describe('myComponent', () => {
test('should renders correctly', async () => {
const mProps = {
text: 'text',
list: [],
getData: jest.fn().mockResolvedValueOnce([{ id: 1 }, { id: 2 }, { id: 3 }])
};
let component;
await act(async () => {
component = renderer.create(<MyComponent {...mProps}></MyComponent>);
});
expect(component.toJSON()).toMatchSnapshot();
});
});
Unit test result:
PASS src/stackoverflow/57778786/index.spec.tsx
myComponent
✓ should renders correctly (29ms)
console.count src/stackoverflow/57778786/index.tsx:13
useEffect: 1
console.count src/stackoverflow/57778786/index.tsx:15
fetchData: 1
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 passed, 1 total
Time: 3.557s, estimated 8s
index.spec.tsx.snap:
// Jest Snapshot v1
exports[`myComponent should renders correctly 1`] = `
<div>
rows count:
3
</div>
`;
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57778786

React/Redux App stops processing MapDispatchToProps functions

So Im having an issue with a react/redux project. The program stops outputting information when the first onSetTechnicianXXX call is made. The interesting thing is I successfully perform multiple onSetGetData actions as well as an onSetTechnicianId (inside the parent file). I get no errors. Below is the debug code:
console.log("1");
const technician = fetchedTechnicians[0];
console.log("Tech.Intake.Name: " + technician.intakeData.name);
console.log("2");
const name = technician.intakeData.name;
console.log("Name: " + name);
this.onSetTechnicianName(name);
console.log("3");
this.onSetTechnicianName(technician.intakeData.name);
console.log("4");
this.onSetTechnicianCerts(technician.intakeData.certifications);
console.log("5");
this.onSetTechnicianPhone(technician.intakeData.phone);
console.log("6");
this.onSetTechnicianEmail(technician.intakeData.email);
console.log("7");
this.onSetTechnicianServicebay(technician.intakeData.servicebay);
console.log("8");
========Output from the Chrome DevTools Console========
1 TechnicianEditForm.js:118
Tech.Intake.Name: Joe Technician TechnicianEditForm.js:120
2 TechnicianEditForm.js:121
Name: Joe Technician TechnicianEditForm.js:123
========Output from the Chrome Redux DevTool=========
selectedTechnicianId(pin): "-LnECtv2Ms40OaFBu2t0"
getdata(pin): false
name(pin): ""
certifications(pin): ""
phone(pin): ""
email(pin): ""
servicebay(pin): ""
These are the React/Redux files for processing the state
===========actionTypes.js:==========================
export const SET_TECHNICIANID = 'SET_TECHNICIANID';
export const SET_GETDATA = 'SET_GETDATA';
export const SET_TECHNICIAN_NAME = 'SET_TECHNICIAN_NAME';
export const SET_TECHNICIAN_CERTS = 'SET_TECHNICIAN_CERTS';
export const SET_TECHNICIAN_PHONE = 'SET_TECHNICIAN_PHONE';
export const SET_TECHNICIAN_EMAIL = 'SET_TECHNICIAN_EMAIL';
export const SET_TECHNICIAN_SERVICEBAY = 'SET_TECHNICIAN_SERVICEBAY';
=================actions.js:===========================
import * as actionTypes from './actionTypes';
export const setTechnicianId = (selectedTechnicianId) => {
return {
type: actionTypes.SET_TECHNICIANID,
selectedTechnicianId: selectedTechnicianId
};
};
export const setGetData = (getdata) => {
return {
type: actionTypes.SET_GETDATA,
getdata: getdata
};
};
export const setTechnicianName = (name) => {
return {
type: actionTypes.SET_TECHNICIAN_NAME,
name: name
};
};
##### Others Not Shown But Simular #####
=================reducer.js:================
import * as actionTypes from './actionTypes';
import { updateObject } from './utility';
const initialState = {
selectedTechnicianId: '',
getdata: false,
name: '',
certifications: '',
phone: '',
email: '',
servicebay: ''
}
const baseReducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.SET_TECHNICIANID:
return updateObject(state, { selectedTechnicianId: action.selectedTechnicianId });
case actionTypes.SET_GETDATA:
return updateObject(state, { getdata: action.getdata });
case actionTypes.SET_TECHNICIAN_NAME:
return updateObject(state, { name: action.name });
##### Others Not Shown But Simular #####
=======================index.js:=====================
export {
setTechnicianId,
setGetData,
setTechnicianName,
setTechnicianCerts,
setTechnicianPhone,
setTechnicianEmail,
setTechnicianServicebay,
} from './actions';
===========Part of the TechnicianEditForm.js============
const mapStateToProps = state => {
return {
selectedTechnicianId: state.selectedTechnicianId,
getdata: state.getdata,
name: state.name,
certifications: state.certifications,
phone: state.phone,
email: state.email,
servicebay: state.servicebay
};
};
const mapDispatchToProps = dispatch => {
return {
onSetTechnicianId: (selectedTechnicianId) => dispatch(actionTypes.setTechnicianId(selectedTechnicianId)),
onSetGetData: (getdata) => dispatch(actionTypes.setGetData(getdata)),
onSetTechnicianName: (name) => dispatch(actionTypes.setTechnicianName(name)),
onSetTechnicianCerts: (certifications) => dispatch(actionTypes.setTechnicianCerts(certifications)),
onSetTechnicianPhone: (phone) => dispatch(actionTypes.setTechnicianPhone(phone)),
onSetTechnicianEmail: (email) => dispatch(actionTypes.setTechnicianEmail(email)),
onSetTechnicianServicebay: (servicebay) => dispatch(actionTypes.setTechnicianServicebay(servicebay)),
}
};
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(TechnicianEditForm), axios);

Categories

Resources