how to use jest to mock method of react class - reactjs

I want to mock method of react class so that the unit test can run follow the mock function.
React: 16.8.6
jest: 24.8.0
Overview.js
import React from 'react';
export default class Overview extends Component{
test1(){
return {
// fetch api
}
}
test2(){
const result = this.test1();
// do other thing
return result
}
}
overview.test.js
import Overview from './index';
import { mount } from 'enzyme';
import React from 'react';
describe('test Overview',()=>{
const mockResult = {test1:'test1'};
console.info(Overview.prototype) // {}
Overview.prototype.test1=jest.fn(()=>{
return mockResult
});
it('func test2',()=>{
const wrapper = mount(<Overview/>);
const {test2} = wrapper.instance();
expect(mockResult).toEqual(test2())
})
})
Expect: run success
Actual result: run fail, because Overview.prototype cannot override or mock test1 function.
When I tried to print 'Overview.prototype', I get {}. That let me so confuse.
How to mock test1 function and why Overview cannot be overrode?
Please help me.

Try to do this:
it('func test2',()=>{
const wrapper = mount(<Overview/>);
wrapper.instance().test1 = jest.fn(() => mockResult);
expect(wrapper.instance().test2()).toEqual(mockResult);
})

There are plenty of reason to choose different approach than mocking internal methods and checking against internal methods:
it's hard and even impossible somtimes(once method under mock change state or even access variable by closure)
you stick to implementation details so even smallest refactoring(renaming internal method or property name in state) make you update tons of tests
it makes you even more confident in your component(say what if test1() stopped to call fetch()? but your tests with mocking it would not even know that component is broken)
Here is different approach: mock only external API and communicate only through public interface(for React component it's props and render() result you may access with Enzyme's methods like .find(), .filter(), .text() etc)
There are several packages that mocks global fetch() like fetch-mock but you actually may mock it on your own(don't forget to mock it with Promise not plain data):
global.fetch = jest.fn();
beforeEach(() => {
// important for mocks to keep them fresh on each test case run
global.fetch.mockClear();
});
it('renders error if fetching failed', async () => {
global.fetch.mockReturnValue(Promise.reject({}));
const wrapper = shallow(<Overview />);
wrapper.find('.some-button-to-click').props().onClick();
await Promise.resolve(); // let's wait till mocked fetch() is resolved
expect(wrapper.find('.error').text()).toEqual('Unable to load users. Try later.');
});

Related

Is it possible to mock functions outside of the test file for multiple tests to use?

Thanks in advance
Issue
I have some functions that need to be mocked in every test file. However, only in some of those files, do I actually care to evaluate or test those mock functions.
For example, let's say, that when my app renders, it immediately fetches some data In A.test.tsx I want to mock that fetch and verify that it was called.
But, in B.test.tsx, I still need to mock that fetch, but I don't care to verify that it was called.
Goal
I would like the setup to look something like this:
setup.ts
import * as SomeAPI from 'api';
const setup = () => {
jest.spyOn(SomeAPI, 'fetchData');
return { SomeAPI };
};
export default setup;
A.test.tsx
const { SomeAPI } = setup();
beforeEach(() => {
jest.clearAllMocks();
});
test('data should be fetched when app renders', async () => {
RenderWithProviders(<App />);
expect(SomeAPI.fetchData).toHaveBeenCalled();
});
B.test.tsx
setup(); // Don't destructure because I don't care to test the mock
beforeEach(() => {
jest.clearAllMocks();
});
test('test some other stuff', async () => {
// In this case, the fetchData still needs to be mocked<br>
// because the rest of the app depends on it
RenderWithProviders(<App />);
expect(someElement).toBeInTheDocument();
});
My Current Problem
My problem is that while I'm trying to attempt this way of returning mocked functions... If that mocked function is used more than once in the same test file, the jest.clearAllMocks() seems not to have an effect. I assume because the setup is happening outside of the test?
Is this possible to setup mocks outside of the test and only destructure them when needed?

Jest: Wait for called .then of async method in componentDidMount

I am currently stuck on writing a test of my React-App.
I have an async call in my componentDidMount method and are updating the state after returning. However, I do not get this to work.
I have found several solutions and None seems to work as expected. Below is the nearest point I have come to.
App:
class App extends Component<{}, IState> {
state: IState = {
fetchedData: false
};
async componentDidMount() {
await thing.initialize();
this.test();
}
test = () => {
this.setState({ fetchedData: true })
};
render() {
return this.state.fetchedData ? (
<div>Hello</div>
) : (
<Spinner />
);
}
}
The test
it('Base test for app', async () => {
const spy = spyOn(thing, 'initialize').and.callThrough(); // just for debugging
const wrapper = await mount(<App />);
// await wrapper.instance().componentDidMount(); // With this it works, but componentDidMount is called twice.
wrapper.update();
expect(wrapper.find('Spinner').length).toBe(0);
});
Well, so...thing.initialize is called (it is an async method that fetches some stuff).
If I do explicitly call wrapper.instance().componentDidMount() then it will work, but componentDidMount will be called twice.
Here are my ideas that I have tried but None succeeded:
Spying on thing.initialize() -> I did not find out how I proceed with the test after the method has been called and finished.
Spying on App.test -> The same here
Working with promises instead of async await
At the beginning, I had an thing.initialize().then(this.test) in my componentDidMount
It can't be much, but can someone tell me which piece I am missing?
if this is integration test you better to follow awaiting approach that say Selenium use: that is, just wait until some element appears or timeout reached. How it should be coded depends on library you use(for Puppeter it should be waitForSelector).
Once it's about unit test then I suggest you different approach:
mock every single external dependencies with Promise you control(by your code it's hard to say if automatic mock will work or you need to compose mock factory but one of them or both will help)
initialize element(I mean just run shallow() or mount())
await till your mocks are resolved(with extra await, using setTimeout(... ,0) or flush-promises will work, check how microtasks/macrotasks works)
assert against element's render and check if your mocks has been called
And finally:
setting state directly
mocking/spying on internal methods
verifying against state
are all lead to unstable test since it's implementation details you should not worry about during unit-testing. And it's hard to work with them anyway.
So your test would look like:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('loads data and renders it', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.resolve(/*data you expect to return*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(1);
})
or you may test how component behaves on rejection:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('renders error message on loading failuer', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.reject(/*some error data*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
expect(wrapper.find(SomeErrorMessage)).toHaveLength(1);
})

Why does Jest run assertions before componentDidMount finishes executing (using React Test Renderer)?

Here is the repo for this question if you want to directly reproduce.
I have a freshly created react-native project (I think it is not important whether it is React or React-Native for this question). I have one single component App.js:
import React, { Component } from 'react';
import { View } from 'react-native';
import actions from './actions';
export class App extends Component {
async componentDidMount() {
console.log('In CDM');
await actions.funcOne();
await actions.funcTwo();
console.log('Finished CDM');
}
render() {
return <View />;
}
}
Here are two functions that that component is importing from actions.js:
const funcOne = async () => {
console.log('One');
};
const funcTwo = async () => {
console.log('Two');
};
export default { asyncOne: funcOne, asyncTwo: funcTwo };
And here is a test I wrote:
import React from 'react';
import { App } from '../App';
import renderer from 'react-test-renderer';
import actions from '../actions';
const spyOne = jest.spyOn(actions, 'funcOne');
const spyTwo = jest.spyOn(actions, 'funcTwo');
describe('App ', () => {
test('does async stuff in expected order', async () => {
console.log('Starting test');
const tree = await renderer.create(<App />);
console.log('About to expect');
expect(spyOne).toHaveBeenCalled();
console.log('Expect one to have been called');
expect(spyTwo).toHaveBeenCalled();
console.log('Expect two to have been called');
expect(tree).toMatchSnapshot();
});
});
Here is the result of running the test:
As can be seen, the second expect assertion is being called before the function funcTwo is executed in componentDidMount.
What I am actually trying to accomplish is I have a much more complex component that executes an async function (that makes API calls for example) in componentDidMount. I want my test to create the component tree, and assert that the component actually did make the calls to the relevant functions.
I actually found a "solution" (it makes my tests pass and the console.logs appear in the correct order, but I don't understand why it works. The solution is to add the line await (() => new Promise(setImmediate))(); in the test file right after the line with await renderer.create.
**So, I don't want only a solution (though if you have an ideal solution please provide it). I want to know what's going on here, why does the original code not work as expected? **
async / await is just syntactic sugar for Promises and generators.
When you call await you essentially queue the rest of the function in a then attached to the Promise you are awaiting.
This means that when the Promise resolves the rest of the function is added to PromiseJobs queue.
Promise callbacks in the PromiseJobs queue run after the current message completes...which means that any synchronous code will complete before the callback has a chance to run.
In this case this line runs:
await actions.funcOne();
...which calls funcOne synchronously. It resolves immediately so the rest of componentDidMount is placed in the PromiseJobs queue and execution returns to the test. (Note that calling await on renderer.create doesn't wait for the Promise returned by componentDidMount).
The rest of the test is synchronous so it runs the first expect which passes, then runs the second expect which fails since the rest of componentDidMount is still waiting in the PromiseJobs queue.
To get the test to pass, you just need to give the callback queued in PromiseJobs a chance to run.
As you found, that can be done with this line:
await (() => new Promise(setImmediate))();
...but even easier is just to await a resolved Promise:
await Promise.resolve();
This will queue the rest of the test at the back of the PromiseJobs queue behind the callback that will call actions.funcTwo and the test will pass.
Here is a slightly simplified example to demonstrate:
import * as React from 'react';
import renderer from 'react-test-renderer';
const f1 = jest.fn();
const f2 = jest.fn();
class App extends React.Component {
async componentDidMount() {
await f1();
await f2();
}
render() { return null; }
}
test('does async stuff in expected order', async () => {
const tree = renderer.create(<App />);
expect(f1).toHaveBeenCalled(); // Success!
await Promise.resolve(); // <= let any callbacks in PromiseJobs run
expect(f2).toHaveBeenCalled(); // Success!
});

Return value of a mocked function does not have `then` property

I have the following async call in one of my React components:
onSubmit = (data) => {
this.props.startAddPost(data)
.then(() => {
this.props.history.push('/');
});
};
The goal here is to redirect the user to the index page only once the post has been persisted in Redux (startAddPost is an async action generator that sends the data to an external API using axios and dispatches another action that will save the new post in Redux store; the whole thing is returned, so that I can chain a then call to it in the component itself). It works in the app just fine, but I'm having trouble testing it.
import React from 'react';
import { shallow } from 'enzyme';
import { AddPost } from '../../components/AddPost';
import posts from '../fixtures/posts';
let startAddPost, history, wrapper;
beforeEach(() => {
startAddPost = jest.fn();
history = { push: jest.fn() };
wrapper = shallow(<AddPost startAddPost={startAddPost} history={history} />);
});
test('handles the onSubmit call correctly', () => {
wrapper.find('PostForm').prop('onSubmit')(posts[0]);
expect(startAddPost).toHaveBeenLastCalledWith(posts[0]);
expect(history.push).toHaveBeenLastCalledWith('/');
});
So I obviously need this test to pass, but it fails with the following output:
● handles the onSubmit call correctly
TypeError: Cannot read property 'then' of undefined
at AddPost._this.onSubmit (src/components/AddPost.js:9:37)
at Object.<anonymous> (src/tests/components/AddPost.test.js:25:46)
at process._tickCallback (internal/process/next_tick.js:109:7)
So how can I fix this? I suspect this is a problem with the test itself because everything works well in the actual app. Thank you!
Your code is not testable in the first place. You pass in a callback to the action and execute it after saving the data to the database like so,
export function createPost(values, callback) {
const request = axios.post('http://localhost:8080/api/posts', values)
.then(() => callback());
return {
type: CREATE_POST,
payload: request
};
}
The callback should be responsible for the above redirection in this case. The client code which uses the action should be like this.
onSubmit(values) {
this.props.createPost(values, () => {
this.props.history.push('/');
});
}
This makes your action much more flexible and reusable too.
Then when you test it, you can pass a stub to the action, and verify whether it is called once. Writing a quality, testable code is an art though.
The problem with your code is that the startAddPost function is a mock function which does not return a Promise, but your actual this.props.startAddPost function does return a Promise.
That's why your code works but fails when you try to test it, leading to the cannot read property.... error.
To fix this make your mocked function return a Promise like so -
beforeEach(() => {
startAddPost = jest.fn().mockReturnValueOnce(Promise.resolve())
...
});
Read more about mockReturnValueOnce here.

How to wait for setState in componentDidMount to resolve when testing with enzyme?

I am trying to test a React component that runs some asynchronous code and calls setState in componentDidMount.
Here is my react component that I want to test:
/**
*
* AsyncComponent
*
*/
import React from 'react';
class AsyncComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loaded: false,
component: null,
};
}
componentDidMount() {
this.props.component.then(component => {
this.setState({
loaded: true,
component: component.default ? component.default : component,
});
});
}
render() {
if (!this.state.loaded) {
return null;
}
const Component = this.state.component;
const { component, ...rest } = this.props;
return <Component {...rest} />;
}
}
export default AsyncComponent;
Here is the test case. I am using jest and enzyme.
import React from 'react';
import { mount } from 'enzyme';
import AsyncComponent from '../index';
const TestComponent = () => <div>Hello</div>;
describe('<AsyncComponent />', () => {
it('Should render loaded component.', () => {
const promise = Promise.resolve(TestComponent);
const rendered = mount(<AsyncComponent component={promise} />);
expect(rendered.state().loaded).toBe(true);
});
});
The test fails because state.loaded is still set to false. Is there a way I can make sure that AsyncComponent has fully loaded before calling expect?
I can get it to work if I wrap the expect assertion in a setTimeout, but that seems like a rather hacky way to do it. How should I go about doing this?
Approach with setTimeout is totally fine. Since it's a macrotask, it will be guaranteed to be called only after microtasks queue becomes empty - in other words when all the Promises are resolved and .then is processed
With this approach your test will legitimately pass after (we assume all server calls are properly mocked):
You add more calls either in sequence .then(...).then(... Another call).then(...)
You replace server call with some sync operation(reading from Redux store or local storage)
Refactor component to function version with hooks
The only thing I'd change - instead of checking state data(that would not with to function components and is fragile even to class components), I'd check .isEmptyRenderer() that must be true before timeout(so until promises are all settled) and false inside of timeout
More on macrotask/microtask difference: https://javascript.info/event-loop
[UPD] as #Estus Flask noticed below, relying on setTimeout in generic case might lead to callback hell(setTimeout after first action, then nested setTimeout to do next step etc). To avoid that we can use
await new Promise(resolve => { setImmediate(resolve); });
to flush microtasks queue. Or use tiny flush-promises package that does the same under the hood but looks lighter:
await flushPromises();
You need to notify jest about the promise either by using async/await or return the promise from the test, have a look at the docs
describe('<AsyncComponent />', () => {
it('Should render loaded component.', async() => {
const promise = Promise.resolve(TestComponent);
const rendered = mount(<AsyncComponent component={promise} />);
await promise
expect(rendered.state().loaded).toBe(true);
});
});
I encountered the same problem, and I came up with some clumsy solution, I have some function call in componentDidMount and I wanted to check if that function has been called, so that code worked for me
const loadFiltersTree = jest.fn()
const wrapper = shallow(<FilterTree loadFiltersTree={loadFiltersTree} />)
jest.useFakeTimers()
jest.runAllTimers()
setImmediate(() => {
expect(loadFiltersTree.mock.calls.length).toEqual(1)
})
Breaking a promise chain is a common antipattern. As a rule of thumb, a function that uses promises should return a resulting promise to chain, unless this causes a problem. This guarantees that there won't be race conditions when caller function chains a promise. One of reasons for this is improved testability. This also applies to lifecycle hooks like componentDidMount:
componentDidMount() {
return this.props.component.then(...)
}
Asynchronous Jest test should chain all promises in use and return a promise. async..await is a practical way to do this.
In Enzyme, shallow rendering allows to disable automatic componentDidMount call and chain a promise that lifecycle hook returns:
const wrapper = shallowMount(<AsyncComponent component={promise} />,
{ disableLifecycleMethods: true });
await wrapper.instance().componentDidMount();
expect(wrapper.state().loaded).toBe(true);
This can also be done with full rendering by spying on componentDidMount:
jest.spyOn(AsyncComponent.prototype, 'componentDidMount');
const wrapper = mount(<AsyncComponent component={promise} />);
expect(wrapper.instance().componentDidMount).toHaveBeenCalledTimes(1);
await wrapper.instance().componentDidMount.mock.results[0].value;
expect(wrapper.state().loaded).toBe(true);

Resources