How to test React component with AJAX in componentWillMount - reactjs

I have a React component where some data is fetched via HTTP during componentWillMount.
This is done pretty much like explained here using the Redux Thunk middleware.
The request is done with axios.
So initially a loading state is set and the data will be set as soon as the request returns.
class SomeComponent extends Component {
//...
componentWillMount() {
store.dispatch(fetchInitialData())
}
// ...
}
The action doing the fetch:
export function fetchInitialData() {
return dispatch => {
return axios.get('https://our.api')
.then(res => {
dispatch(handleSuccess(res.data));
})
.catch(error => dispatch(handleError(error)));
}
}
The test is written with Jest and Enzyme and axios is mocked with axios-mock-adapter.
import Enzyme, { mount } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import configureMockStore from 'redux-mock-store'
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
import thunk from 'redux-thunk'
import response from 'testdata/api/response.json'
Enzyme.configure({
adapter: new Adapter()
});
function createMockStore() {
const mockStore = configureMockStore([thunk]);
return mockStore({});
}
describe('some component', () => {
it('should render ...', () => {
const mock = new MockAdapter(axios);
mock.onGet('/our.api').reply(200, response);
const store = createMockStore();
const enzymeWrapper = mount(<SomeComponent store={store}/>);
console.log('enzymeWrapper', enzymeWrapper.html());
});
});
The problem is that the html of enzymeWrapper always contains only the representation of the initial state. The initial HTML never changes according to the new state from the response.
When I add logging to the reducers and services I see that the mocked requests executes and the state is filled with the mocked response.
But the component does not re-render with the new state data.
I want to point out that this is only an issue in the test. In the actual app everything works fine.
To me it seems that I need to somehow wait for the state changes to get applied. But I don't know how.

Related

Reproducable asynchronous bug found in #testing-library/react

Unless I'm mistaken, I believe I've found a bug in how rerenders are triggered (or in this case, aren't) by the #testing-library/react package. I've got a codesandbox which you can download and reproduce in seconds:
https://codesandbox.io/s/asynchronous-react-redux-toolkit-bug-8sleu4?file=/README.md
As a summary for here, I've just got a redux store and I toggle a boolean value from false to true after some async activity in an on-mount useEffect in a component:
import React, { useEffect } from "react";
import { useAppDispatch } from "../hooks/useAppDispatch";
import { setMyCoolBoolean } from "../redux/slices/exampleSlice";
import AnotherComponent from "./AnotherComponent";
export default function InnerComponent() {
const dispatch = useAppDispatch();
const fetchSomeData = async () => {
await fetch("https://swapi.dev/api/people");
dispatch(setMyCoolBoolean(true));
};
// on mount, set some values
useEffect(() => {
fetchSomeData();
}, []);
return <AnotherComponent />;
}
Then, in a different component, I hook into that store value with useAppSelector hook and then useEffect to do something local there (dumb example, but it illustrates my point.):
import { useEffect, useState } from "react";
import { useAppSelector } from "../hooks/useAppSelector";
export default function AnotherComponent() {
const { myCoolBoolean } = useAppSelector((state) => state.example);
const [localBoolean, setLocalBoolean] = useState(false);
// when myCoolBoolean changes, set the local boolean state value in this component
// somewhat a dumb example but it illustrates
// the failure of react-testing-library
useEffect(() => {
// only do something in this component
// if myCoolBoolean changes to true
if (myCoolBoolean) {
console.log("SET TO TRUE!");
setLocalBoolean(myCoolBoolean);
}
}, [myCoolBoolean]);
if (localBoolean) {
return <span data-testid="NEW">I'm new</span>;
}
return <span data-testid="ORIGINAL">I'm original</span>;
}
In result, my test would like to see if the 'new' value is ever shown. Despite issuing rerender, you will see the test fails:
import React from "react";
import { render } from "#testing-library/react";
import App from "../../src/App";
import { act } from "react-test-renderer";
import 'whatwg-fetch'
test("On mount, boolean value changes, causing our new span to show up", async () => {
const { getByTestId, rerender } = render(<App />);
await act(async () => {
// expect(getByTestId("ORIGINAL")).toBeTruthy();
// No matter how many times you call rerender here,
// you'll NEVER see the "NEW" test id (and thus corresponding <span> element) appear in the document
// despite this being the case in any standard browser
await rerender(<App />);
// If you comment this line below out, the test passes fine.
// test ID "ORIGINAL" is found, but "NEW" is never found!!!!
expect(getByTestId("NEW")).toBeTruthy();
});
});
Behaviour is totally as expected in a browser, but fails in my jest test. Can anybody guide me on how to get my test to pass? As far as I know, the code and implementations of my React components and Redux are the cleanest and best practices that are currently out there, so I'm more expecting this is a gross misunderstanding on my part of how #testing-library works, though I thought rerender would do the trick.
I've apparently misunderstood how react-testing-library works under the hood. You don't even need to use rerender or act at all! Simply using a waitFor with await / async is enough to trigger the on mount logic and subsequent rendering:
import React from "react";
import { findByTestId, render, waitFor } from "#testing-library/react";
import App from "../../src/App";
import { act } from "#testing-library/react-hooks/dom";
import "whatwg-fetch";
test("On mount, boolean value changes, causing our new span to show up", async () => {
const { getByTestId, rerender, findByTestId } = render(<App />);
// Works fine, as we would expect
expect(getByTestId("ORIGINAL")).toBeTruthy();
// simply by using 'await' here, react-testing-library must rerender somehow
// note that 'act' isn't even used or needed either!
await waitFor(() => getByTestId("NEW"));
});
Another case of "overthinking it" gone bad...

Redux toolkit - testing store.dispatch causes infinite loop

I want to test async action changes store state correctly.
However, when I make configure store and use store.dispatch, it causes infinite loop and test ends with timeout error.
Here is my code.
import { configureStore } from "#reduxjs/toolkit";
import * as APIs from "../../common/APIs";
import { IRepository, repositoryFactory } from "../../common/Interfaces";
import reposReducer, { fetchRepositories, reposInitialState, toBeLoaded } from "./reposSlice";
import store from "../../app/store";
import authReducer from "../auth/authSlice";
jest.mock("../../common/APIs");
const mockedAPIs = APIs as jest.Mocked<typeof APIs>;
describe("reposSlice", () => {
const store = configureStore({
reducer: {
repos: reposReducer,
},
});
it("Should fetch repositories correctly", (done) => {
const repository : IRepository = repositoryFactory();
mockedAPIs.getRepositories.mockResolvedValue([repository]);
store.dispatch(fetchRepositories("")).then(() => {
expect(store.getState().repos.repoList.length).toBe(1);
});
});
});
I think my async action "fetchRepositories" act well.
Because when I write
console.log(current(state));
inside the builder.addCase, it print correct state.
Thanks.

how properly mock multiple useSelector hooks

my component has multiple selectors:
import { useSelector } from 'react-redux'
...
const data1 = useSelector(xxxxx)
const data2 = useSelector(yyyyy)
How properly mock each in test file?
import { useSelector } from 'react-redux'
jest.mock('react-redux', () => ({
useSelector: jest.fn()
}))
....
useSelector.mockImplementation(() => ({
dataready: true
}))
which selector it's really mocking in this case?
Don't mock the selector. You want to test the integration between Redux and React components, not the Redux implementation of selectors. If you use react-testing-library it's pretty simple to hijack the render() method and implement your store using a Redux Provider component. Here are the docs for testing Connected Components.
Here's your test re-written with the user in mind:
import { render } from '../../test-utils' // <-- Hijacked render
it('displays data when ready', { // <-- behavior explanation
const {getByTestId} = render(<YourComponent />, {
initialState: {
dataready: true // <-- Pass data for selector
}
})
expect(getByTestId('some-testId')).toBeTruthy(); // <-- Check that something shows based on selector
})
import * as redux from 'react-redux';
...
beforeEach(() => {
jest
.spyOn(redux, 'useSelector')
.mockReturnValueOnce(xxxx)
.mockReturnValueOnce(yyyy);
});
You'd want to do something like this, to get your spy, and then check on what it is called with and mockImplementation to prevent async if thats an issue for you, i'd suggest you provide state via the render function, rather than mock a selector implementation though.
import { useDispatch, useSelector } from 'react-redux';
const reactRedux = { useDispatch, useSelector };
const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');

Mocking external class method inside React component with jest

I'm trying to test component method, which inside performing network call to external resources. After reading docs I still can't figure out how to do so. Can anyone help? Here is my code(some parts hidden for brevity):
My component:
import React from 'react'
import ResourceService from '../../modules/resource-service'
export default class SliderComponent extends React.Component {
setActiveSlide = (activeSlide) => {
ResourceService.getData({
id: activeSlide,
}).then((data) => {
if (data) {
this.setState({
data,
})
}
})
}
}
Resource service:
import axios from 'axios'
export default class ResourceService {
static getData(params) {
return axios.post('/api/get_my_data', params)
.then((resp) => resp.data)
}
}
Desired test (as I understand it):
import React from 'react'
import { mount, configure } from 'enzyme'
import SliderComponent from '../../../app/components/slider'
test('SliderComponent changes active slide when setActiveSlide is
called', () => {
const wrapper = mount(
<SliderComponent />
);
wrapper.instance().setActiveSlide(1);
// some state checks here
});
I need mock ResourceService.getData call inside SliderComponent, and I really can't understand ho to do it...
You can import your ResourceService in your test and mock the method getData with jest.fn(() => ...). Here is an example:
import React from 'react'
import { mount, configure } from 'enzyme'
import ResourceService from '../../../modules/resource-service'
import SliderComponent from '../../../app/components/slider'
test('SliderComponent changes active slide when setActiveSlide is
called', () => {
// you can set up the return value, you can also resolve/reject the promise
// to test different scnarios
ResourceService.getData = jest.fn(() => (
new Promise((resolve, reject) => { resolve({ data: "testData" }); }));
const wrapper = mount(<SliderComponent />);
wrapper.instance().setActiveSlide(1);
// you can for example check if you service has been called
expect(ResourceService.getData).toHaveBeenCalled();
// some state checks here
});
try using axios-mock-adapter to mock the postreq in your test.
It should look something like this (may need a few more tweaks):
import React from 'react'
import { mount, configure } from 'enzyme'
import SliderComponent from '../../../app/components/slider'
import axios from'axios';
import MockAdapter = from'axios-mock-adapter';
test('SliderComponent changes active slide when setActiveSlide is
called', () => {
let mock = new MockAdapter(axios)
//you can define the response you like
//but your params need to be accordingly to when the post req gets called
mock.onPost('/api/get_my_data', params).reply(200, response)
const wrapper = mount(
<SliderComponent />
);
wrapper.instance().setActiveSlide(1);
// some state checks here
});
make sure to check the docs of axios-mock-adapter

is there another way to mock component's mapDispatchToProps when using Jest

I currently have a component like so:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getDataAction } from ' './my-component';
export class MyComponent extends { Component } {
componentWillMount() {
this.props.getData();
}
render(){
<div>
this.props.title
</div>
}
}
const mapStateToProps = (state) => ({
title: state.title
});
const mapDispatchToProps = (dispatch) ({
getData() {
dispatch(getDataAction());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
and I am trying to shallow render test it using jest and enzyme.
test:
import React from 'react';
import { shallow } from 'enzyme';
import { MyComponent } from './index';
it('renders without crashing', () => {
shallow(<MyComponent getData={jest.fn()} />);
});
My question is, is this the conventional way to mock? Jest official docs don't mention specifically about mocking props and this post Using Jest to mock a React component with props is about testing with full mounting instead.
Is there another way to mock dispatchToProps? In this example there is only one, but what if I have a lot of functions in dispatchToProps?
Side Question: in my real file, I have a reference to a value like this.props.information.value which I expect to throw an error like cannot get value of undefined since information is not mocked/defined, but it doesn't. It's only when functions are not present that an error is thrown.
You can export mapDispatchToProps and write tests for it by importing it in your tests.
Add export { mapDispatchToProps }; at the end of your MyComponent.js
Create MyComponent.tests.js file beside MyComponent.js
import configureMockStore from 'redux-mock-store';
import thunkMiddleware from 'redux-thunk';
import { mapDispatchToProps } from './MyComponent';
const configMockStore = configureMockStore([thunkMiddleware]);
const storeMockData = {};
const mockStore = configMockStore(storeMockData);
describe('mapDispatchToProps', () => {
it('should map getDataAction action to getData prop', () => {
// arrange
const expectedActions = [getDataAction.type];
const dispatchMappedProps = mapDispatchToProps(mockStore.dispatch);
// act
dispatchMappedProps.getData();
// assert
expect(mockStore.getActions().map(action => action.type)).toEqual(expectedActions);
}
});
Here I have used thunk, just to let you know that how to do it if there are middlewares configured in your store setup.
Here getDataAction can also be a function instead of a simple action like { type: 'FETCH_DATA' } if you are using middlewares like thunks. However, the approach to test is same except that you will create expectedActions with explicit action types like const expectedActions = ['FETCH_CONTACTS']
Here FETCH_CONTACT is another action dispatched in your thunk i.e getDataAction

Resources