The Problem:
I have a simple React component I'm using to learn to test components with Jest and Enzyme. As I'm working with props, I added the prop-types module to check for properties in development. prop-types uses console.error to alert when mandatory props are not passed or when props are the wrong data type.
I wanted to mock console.error to count the number of times it was called by prop-types as I passed in missing/mis-typed props.
Using this simplified example component and test, I'd expect the two tests to behave as such:
The first test with 0/2 required props should catch the mock calling twice.
The second test with 1/2 required props should catch the mock called once.
Instead, I get this:
The first test runs successfully.
The second test fails, complaining that the mock function was called zero times.
If I swap the order of the tests, the first works and the second fails.
If I split each test into an individual file, both work.
console.error output is suppressed, so it's clear it's mocked for both.
I'm sure I am missing something obvious, like clearing the mock wrong or whatever.
When I use the same structure against a module that exports a function, calling console.error some arbitrary number of times, things work.
It's when I test with enzyme/react that I hit this wall after the first test.
Sample App.js:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class App extends Component {
render(){
return(
<div>Hello world.</div>
);
}
};
App.propTypes = {
id : PropTypes.string.isRequired,
data : PropTypes.object.isRequired
};
Sample App.test.js
import React from 'react';
import { mount } from 'enzyme';
import App from './App';
console.error = jest.fn();
beforeEach(() => {
console.error.mockClear();
});
it('component logs two errors when no props are passed', () => {
const wrapper = mount(<App />);
expect(console.error).toHaveBeenCalledTimes(2);
});
it('component logs one error when only id is passed', () => {
const wrapper = mount(<App id="stringofstuff"/>);
expect(console.error).toHaveBeenCalledTimes(1);
});
Final note: Yeah, it's better to write the component to generate some user friendly output when props are missing, then test for that. But once I found this behavior, I wanted to figure out what I'm doing wrong as a way to improve my understanding. Clearly, I'm missing something.
I ran into a similar problem, just needed to cache the original method
const original = console.error
beforeEach(() => {
console.error = jest.fn()
console.error('you cant see me')
})
afterEach(() => {
console.error('you cant see me')
console.error = original
console.error('now you can')
})
Given the behavior explained by #DLyman, you could do it like that:
describe('desc', () => {
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterAll(() => {
console.error.mockRestore();
});
afterEach(() => {
console.error.mockClear();
});
it('x', () => {
// [...]
});
it('y', () => {
// [...]
});
it('throws [...]', () => {
shallow(<App />);
expect(console.error).toHaveBeenCalled();
expect(console.error.mock.calls[0][0]).toContain('The prop `id` is marked as required');
});
});
What guys wrote above is correct. I've encoutered similar problem and here's my solution. It takes also into consideration situation when you're doing some assertion on the mocked object:
beforeAll(() => {
// Create a spy on console (console.log in this case) and provide some mocked implementation
// In mocking global objects it's usually better than simple `jest.fn()`
// because you can `unmock` it in clean way doing `mockRestore`
jest.spyOn(console, 'log').mockImplementation(() => {});
});
afterAll(() => {
// Restore mock after all tests are done, so it won't affect other test suites
console.log.mockRestore();
});
afterEach(() => {
// Clear mock (all calls etc) after each test.
// It's needed when you're using console somewhere in the tests so you have clean mock each time
console.log.mockClear();
});
You didn't miss anything. There is a known issue (https://github.com/facebook/react/issues/7047) about missing error/warning messages.
If you switch your test cases ('...when only id is passed' - the fisrt, '...when no props are passed' - the second) and add such
console.log('mockedError', console.error.mock.calls); inside your test cases, you can see, that the message about missing id isn't triggered in the second test.
For my solutions I'm just wrapping original console and combine all messages into arrays. May be someone it will be needed.
const mockedMethods = ['log', 'warn', 'error']
export const { originalConsoleFuncs, consoleMessages } = mockedMethods.reduce(
(acc: any, method: any) => {
acc.originalConsoleFuncs[method] = console[method].bind(console)
acc.consoleMessages[method] = []
return acc
},
{
consoleMessages: {},
originalConsoleFuncs: {}
}
)
export const clearConsole = () =>
mockedMethods.forEach(method => {
consoleMessages[method] = []
})
export const mockConsole = (callOriginals?: boolean) => {
const createMockConsoleFunc = (method: any) => {
console[method] = (...args: any[]) => {
consoleMessages[method].push(args)
if (callOriginals) return originalConsoleFuncs[method](...args)
}
}
const deleteMockConsoleFunc = (method: any) => {
console[method] = originalConsoleFuncs[method]
consoleMessages[method] = []
}
beforeEach(() => {
mockedMethods.forEach((method: any) => {
createMockConsoleFunc(method)
})
})
afterEach(() => {
mockedMethods.forEach((method: any) => {
deleteMockConsoleFunc(method)
})
})
}
Related
I want to write a test to see if a useEffect function inside my component is called if the correct prop is passed in.
PropDetail.js
import React, { useState, useEffect } from "react";
function PropDetail({ propID }) {
const [propStatus, setPropStatus] = useState(null);
useEffect(() => {
if (!propID) return;
setPropStatus(propID)
}, [propID]);
return <p>{propStatus}</p>
}
export default PropDetail;
PropDetail.test.js
import React from "react";
import { shallow } from "enzyme";
import PropDetail from '../PropDetail'
const props = { }
describe('PropDetail', () => {
const wrapper = shallow(<PropDetail {...props} />)
describe('With no propID', () => {
it('returns null if no propID passed in', () => {
expect(wrapper.html()).toBe(null)
})
})
describe('With propID passed in', () => {
beforeEach(() => {
wrapper.setProps({ propID: 'PROPID' })
})
it('Runs useEffect with propID', () => {
expect(wrapper.setPropStatus().toHaveBeenCalledTimes(1);
})
})
})
The errors i'm getting in the console are 'Matcher error: received value must be a mock or spy function' and 'Received has value: undefined'.
I'm quite new to writing tests so i'm not sure if this is the right starting point or if i'm testing the wrong thing!
You don't want to test a feature of React such as useEffect. You'll want to test your code in a way such as 'what happens when useEffect has been called'.
You could test the value of propStatus upon initial rendering. Then make an update to the propID, then test the value of propStatus. Is the the value you expect or desire?
This way you're testing the actual code you wrote for correctness. When you see a change in propStatus, you'll also know that useEffect was called without directly testing that function.
You can also test for coverage by using yarn test --coverage.
I'm using Jest and Enzyme to test a React functional component.
MyComponent:
export const getGroups = async () => {
const data = await fetch(groupApiUrl);
return await data.json()
};
export default function MyWidget({
groupId,
}) {
// Store group object in state
const [group, setGroup] = useState(null);
// Retrive groups on load
useEffect(() => {
if (groupId && group === null) {
const runEffect = async () => {
const { groups } = await getGroups();
const groupData = groups.find(
g => g.name === groupId || g.id === Number(groupId)
);
setGroup(groupData);
};
runEffect();
}
}, [group, groupId]);
const params =
group && `&id=${group.id}&name=${group.name}`;
const src = `https://mylink.com?${params ? params : ''}`;
return (
<iframe src={src}></iframe>
);
}
When I write this test:
it('handles groupId and api call ', () => {
// the effect will get called
// the effect will call getGroups
// the iframe will contain group parameters for the given groupId
act(()=> {
const wrapper = shallow(<MyWidget surface={`${USAGE_SURFACES.metrics}`} groupId={1} />)
console.log(wrapper.find("iframe").prop('src'))
})
})
The returned src doesn't contain the group information in the url. How do I trigger useEffect and and everything inside that?
EDIT: One thing I learned is the shallow will not trigger useEffect. I'm still not getting the correct src but I've switched to mount instead of shallow
Here's a minimal, complete example of mocking fetch. Your component pretty much boils down to the generic fire-fetch-and-set-state-with-response-data idiom:
import React, {useEffect, useState} from "react";
export default function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
(async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
setUsers(await res.json());
})();
}, []);
return <p>there are {users.length} users</p>;
};
Feel free to run this component in the browser:
<script type="text/babel" defer>
const {useState, useEffect} = React;
const Users = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
(async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
setUsers(await res.json());
})();
}, []);
return <p>there are {users.length} users</p>;
};
ReactDOM.render(<Users />, document.querySelector("#app"));
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
You can see the component initially renders a value of 0, then when the request arrives, all 10 user objects are in state and a second render is triggered showing the updated text.
Let's write a naive (but incorrect) unit test, mocking fetch:
import {act} from "react-dom/test-utils";
import React from "react";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import Users from "../src/Users";
Enzyme.configure({adapter: new Adapter()});
describe("Users", () => {
let wrapper;
let users;
beforeEach(() => {
const mockResponseData = [{id: 1}, {id: 2}, {id: 3}];
users = mockResponseData.map(e => ({...e}));
jest.clearAllMocks();
global.fetch = jest.fn(async () => ({
json: async () => mockResponseData
}));
wrapper = mount(<Users />);
});
it("renders a count of users", () => {
const p = wrapper.find("p");
expect(p.exists()).toBe(true);
expect(p.text()).toEqual("there are 3 users");
});
});
All seems well--we load up the wrapper, find the paragraph and check the text. But running it gives:
Error: expect(received).toEqual(expected) // deep equality
Expected: "there are 3 users"
Received: "there are 0 users"
Clearly, the promise isn't being awaited and the wrapper is not registering the change. The assertions run synchronously on the call stack as the promise waits in the task queue. By the time the promise resolves with the data, the suite has ended.
We want to get the test block to await the next tick, that is, wait for the call stack and pending promises to resolve before running. Node provides setImmediate or process.nextTick for achieving this.
Finally, the wrapper.update() function enables synchronization with the React component tree so we can see the updated DOM.
Here's the final working test:
import {act} from "react-dom/test-utils";
import React from "react";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import Users from "../src/Users";
Enzyme.configure({adapter: new Adapter()});
describe("Users", () => {
let wrapper;
let users;
beforeEach(() => {
const mockResponseData = [{id: 1}, {id: 2}, {id: 3}];
users = mockResponseData.map(e => ({...e}));
jest.clearAllMocks();
global.fetch = jest.fn(async () => ({
json: async () => mockResponseData
}));
wrapper = mount(<Users />);
});
it("renders a count of users", async () => {
// ^^^^^
await act(() => new Promise(setImmediate)); // <--
wrapper.update(); // <--
const p = wrapper.find("p");
expect(p.exists()).toBe(true);
expect(p.text()).toEqual("there are 3 users");
});
});
The new Promise(setImmediate) technique also helps us assert on state before the promise resolves. act (from react-dom/test-utils) is necessary to avoid Warning: An update to Users inside a test was not wrapped in act(...) that pops up with useEffect.
Adding this test to the above code also passes:
it("renders a count of 0 users initially", () => {
return act(() => {
const p = wrapper.find("p");
expect(p.exists()).toBe(true);
expect(p.text()).toEqual("there are 0 users");
return new Promise(setImmediate);
});
});
The test callback is asynchronous when using setImmediate, so returning a promise is necessary to ensure Jest waits for it correctly.
This post uses Node 12, Jest 26.1.0, Enzyme 3.11.0 and React 16.13.1.
With jest you can always mock. So what you need is:
In your unit test mock useEffect from React
jest.mock('React', () => ({
...jest.requireActual('React'),
useEffect: jest.fn(),
}));
That allows to mock only useEffect and keep other implementation actual.
Import useEffect to use it in the test
import { useEffect } from 'react';
And finally in your test call the mock after the component is rendered
useEffect.mock.calls[0](); // <<-- That will call implementation of your useEffect
useEffect has already been triggered and working, the point is that its an async operation. So you need to wait for the fetch to be completed. one of the ways that you can do that is:
1. write your assertion(s)
2. specify the number of assertion(s) in your test, so that jest knows that it has to wait for the operation to be completed.
it('handles groupId and api call ', () => {
// the effect will get called
// the effect will call getGroups
// the iframe will contain group parameters for the given groupId
expect.assertions(1)
const wrapper = shallow(<UsageWidget surface={`${USAGE_SURFACES.metrics}`} groupId={2} />)
wrapper.update()
expect(whatever your expectation is)
});
since in this example i just wrote on assertion,
expect.assertions(1)
if you write more, you need to change the number.
You can set a timeout to asynchronously check if the the expected condition has been met.
it('handles groupId and api call ', (done) => {
const wrapper = shallow(<UsageWidget surface={`${USAGE_SURFACES.metrics}`} groupId={1} />)
setTimeout(() => {
expect(wrapper.find("iframe").prop('src')).toBeTruthy(); // or whatever
done();
}, 5000);
}
The timeout lets you wait for the async fetch to complete. Call done() at the end to signal that the it() block is complete.
You probably also want to do a mock implementation of your getGroups function so that you're not actually hitting a network API every time you test your code.
I'm trying to learn Jest and Enzyme but I'm having a problem I can't find a solution to, this is my test.. it's not very good I know but I'm learning:
import * as apiMock from '../api';
const fakePostId = '1';
const fakePersona = 'Fake';
jest.mock('../api', () => {
return {
fetchAllComments: jest.fn(() => {
return [];
}),
filterComments: jest.fn(() => {
return [];
}),
createCommentObject: jest.fn(() => {
return [];
}),
};
});
test('checks if functions are called after didMount', () => {
const component = shallow(
<Comments postId={fakePostId} currentPersona={fakePersona} />
);
const spySetComments = jest.spyOn(
component.instance(),
'setCommentsFromLocalStorage'
);
component.instance().componentDidMount();
expect(spySetComments).toHaveBeenCalledTimes(1);
//Don't know why these are called 2! times, I can't see why removing componentDidMount makes it 0.
expect(apiMock.fetchAllComments).toHaveBeenCalledTimes(1);
expect(apiMock.filterComments).toHaveBeenCalledTimes(1);
}
The problem is toHaveBeenCalledTimes(1) fails with reason:
Expected mock function to have been called one time, but it was called
two times.
But I don't know why.
setCommentsFromLocalStorage only runs once, and that is the function that sohuld run from componentDidMount and execute these api calls once.
ReactComponent looks like this:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import CreateNewComment from './CreateNewComment';
import SingleComment from './SingleComment';
import * as api from '../api';
class Comments extends Component {
state = {
comments: []
};
componentDidMount() {
this.setCommentsFromLocalStorage();
}
setCommentsFromLocalStorage = (postId = this.props.postId) => {
const fetchedComments = api.fetchAllComments();
const comments = api.filterComments(fetchedComments, postId);
this.setState({ comments });
};
removeComment = commentId => {
api.removeComment(commentId);
this.setCommentsFromLocalStorage();
};
renderCommentList = (comments, currentPersona) =>
comments.map(comment => (
<SingleComment
{...comment}
currentPersona={currentPersona}
key={comment.id}
onClick={this.removeComment}
/>
));
render() {
return (
<div className="py-2">
<h2 className="text-indigo-darker border-b mb-4">Comments</h2>
{this.renderCommentList(this.state.comments, this.props.currentPersona)}
<CreateNewComment
postId={this.props.postId}
author={this.props.currentPersona}
updateComments={this.setCommentsFromLocalStorage}
/>
</div>
);
}
}
Comments.propTypes = {
postId: PropTypes.string.isRequired,
currentPersona: PropTypes.string.isRequired
};
export default Comments;
componentDidMount gets called during shallow().
This means that setCommentsFromLocalStorage gets called which calls fetchAllComments and filterComments all during that initial shallow() call.
api has already been mocked so it records those calls to fetchAllComments and filterComments.
Once that has all happened, the spy is created for setCommentsFromLocalStorage and componentDidMount gets called again (which calls fetchAllComments and filterComments again).
The spy for setCommentsFromLocalStorage then correctly reports that it was called once (since it only existed during the second call to componentDidMount).
The spies on fetchAllComments and filterComments then correctly report that they were called two times since they existed during both calls to componentDidMount.
The easiest way to fix the test is to clear the mocks on fetchAllComments and filterComments before the call to componentDidMount:
apiMock.fetchAllComments.mockClear(); // clear the mock
apiMock.filterComments.mockClear(); // clear the mock
component.instance().componentDidMount();
expect(spySetComments).toHaveBeenCalledTimes(1); // SUCCESS
expect(apiMock.fetchAllComments).toHaveBeenCalledTimes(1); // SUCCESS
expect(apiMock.filterComments).toHaveBeenCalledTimes(1); // SUCCESS
Use beforeEach and afterEach to mock and mockRestore the spies, respectively.
This is explained in the Setup and Teardown section in Jest docs.
I'm using React in my application. I'm making an API call in my componentDidMount but it is conditional. My code in component is
componentDidMount() {
if (!this.props.fetch) {
fetchAPICall()
.then(() => {
/** Do something **/
});
}
}
I've written test as :
it('should not fetch ', () => {
const TFCRender = mount(<Component fetch />);
const didMountSpy = jest.spyOn(TFCRender.prototype, 'componentDidMount');
expect(didMountSpy).toHaveBeenCalledTimes(1);
expect(fetchAPICall).toHaveBeenCalledTimes(0);
});
The test is throwing me error as
TypeError: Cannot read property 'componentDidMount' of undefined
What am I doing wrong and what is the right way to test such case.
From the official docs, you need to spy the component before mounting it.
Following is a working example that I have created with create-react-app. I've also added some comments in the example code:
App.js
import { fetchAPICall } from './api';
class App extends Component {
componentDidMount() {
if (!this.props.fetch) {
fetchAPICall().then(console.log);
}
}
render() {
return <div>Testing the result</div>;
}
}
export default App;
api.js
export const fetchAPICall = () => {
return Promise.resolve('Getting some data from the API endpoint');
};
App.test.js
import Component from './App';
import * as apis from './api'; // assuming you have a separate file for these APIs
// Mock the fetchAPICall, and since the data fetching is asynchronous
// you have to mock its implementation with Promise.resolve()`
apis.fetchAPICall = jest.fn(() => Promise.resolve('test'));
describe('spyOn', () => {
let didMountSpy; // Reusing the spy, and clear it with mockClear()
afterEach(() => {
didMountSpy.mockClear();
});
didMountSpy = jest.spyOn(Component.prototype, 'componentDidMount');
test('should not fetch ', () => {
// Ensure the componentDidMount haven't called yet.
expect(didMountSpy).toHaveBeenCalledTimes(0);
const TFCRender = mount(<Component fetch />);
expect(didMountSpy).toHaveBeenCalledTimes(1);
expect(apis.fetchAPICall).toHaveBeenCalledTimes(0);
});
test('should fetch', () => {
expect(didMountSpy).toHaveBeenCalledTimes(0);
const TFCRender = mount(<Component fetch={false} />);
expect(didMountSpy).toHaveBeenCalledTimes(1);
expect(apis.fetchAPICall).toHaveBeenCalledTimes(1);
});
});
Not sure if this is the best practice, but this is how I usually write my own tests.
Hope this help!
I am doing unit testing for my react component(using jest). I have few methods that are called from the component.
For Example:
export function getText(text, intl) => ((text !== 'NA') ? (intl.formatMessage({ id: value })) : '');
const ReactComponent = (props) => {
const text = getText(text, intl);
return (....);
}
export default injectIntl(ReactComponent);
in test file i have:
import { getText } from 'path';
import { intl } from 'react-intl';
it('should render text', () => {
expect(getText('string', { intl })).toBe('string');
});
When i try to run test, it throws error intl.formatMessage is not a function.
I suppose you already solved the problem yourself. For everyone else struggling with this problem, I would like explain how I solved it using Jest, Enzyme and TypeScript.
The problem: mocking react-intl
'TypeError: intl.formatMessage is not a function'
I experienced the exact same issue. In my case this was caused by mocking the react-intl module. I did not mock formatMessage() which was why I received the undefined function error.
Official helper function does not work with mocked react-intl module
There is an official helper function for testing with Enzyme. However for me it did not work with the mocked react-intl module because IntlProvider and other dependencies of react-intl were also beeing overriden by the mocked module.
Solution
First I created a dummy InjectIntl object, which I gave as prop to the respective component. The object contained the mocked defintion of formatMessage() which returned the default message passed by the calling intl.formatMessage call.
export const getDummyIntlObject = (messages?: any) => {
const intl: InjectedIntl = {
...,
formatMessage: ({ defaultMessage }) => defaultMessage,
...,
}
return intl
}
export const TestComponent: React.FunctionComponent<InjectedIntlProps> = ({ intl }) => {
return (
<div>
<img alt={intl.formatMessage(messages.testImageAltText)} />
...
</div>
)
}
With the returned defaultMessage I could test wether or not the correct string was where I expected it to be.
const props = {
intl: getDummyIntlObject(),
}
describe('TestComponent', () => {
...
test('translations correct', () => {
const w = shallow(<TestComponent {...props} />)
const imageAltText = 'This is a test image'
...
expect(w.find('img').prop('alt')).toContain(imageAltText)
...
})
})
Please checkout my gist for a full code example.
Your getText method requires 2 arguments (text and intl).
const getText = (text, intl) => ((text !== 'NA') ? (intl.formatMessage({ id: value })) : '')
is the same as:
const getText = (text, intl) => {
if (text !== 'NA') {
return intl.formatMessage({ id: value })
}
return ''
}
You are currently calling your test method with getText('string'). Consequently, text (first argument) is different than NA and getText is going to return intl.formatMessage(...).
Since you didn't provide intl, you method will eventually break. That's why you got it throws another error 'formatMessage is not a function' message.
While testing, make sure you are exporting getText so you can use it.
Instead of
const getText = (...)
Use:
export const getText = (...)
so then you can use it as:
import { getText } from 'path/to/gettext'