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!
Related
I'm trying to stub a module using Cypress. Here's what I've tried so far, but is not working.
This is the short version of my component/page
// SomeComponent.jsx
import { useSomething } from './useSomething'
const SomeComponent = () => {
// useSomething is a custom hook
const { data, error } = useSomething()
const renderData = () => {
// map the data into an array of JSX elements
return data.map(...)
}
return (
<div>
{renderData()}
</div>
)
}
export default SomeComponent
Here's how my custom hook looks like
// useSomething.js
import { useState } from 'react'
import { getData } from './db'
export const useSomething = () => {
const [data, setData] = useState({})
const [error, setError] = useState()
useEffect(() => {
getData().then(data => {
setData(data)
}).catch(err => {
setError(error)
})
// ... some other unrelated code here
}, [])
return { data, error }
}
Here's how getData looks like
// getData.js
export const getData = () => {
const data = // some API call from external services
return data
}
The method is exposed via db.js (actually db/index.js)
// db.js
export * from './getData'
I'm trying to stub the getData.js to make the e2e test more consistent. Here's what I did.
// something.spec.js
// I'm writing #src just to keep the code sample here short, it's the same file as the db.js I write above
import * as db from '#src/db'
...
// this is how I try to do the stubbing
cy.stub(db, 'getData').resolves(something)
...
The stubbing above doesn't work. The API call to the external service is still happening when running the test. The documentation itself leads me to deduce that I should write it this way, but it's not working.
You can expose db on the window
// useSomething.js
import { useState } from 'react'
import * as db from './db'
const { getData } = db;
if (window.Cypress) { // only when testing
window.db = db;
}
and in the test
cy.window().then(win => {
cy.stub(win.db, 'getData').resolves(something);
})
Or use intercept to stub the API call.
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 jest and enzyme to test my component. And in this component, I will request data in componentDidMount, and then it will render view with the data.
class App extends React.Component {
async componentDidMount() {
const data = await getData(); // send a request to server to get data
this.setState({
name: data.name,
})
}
render() {
return (
<div>{this.state.name}</div>
)
}
}
It is a very simple component, just only use async function in componentDidMount, and get data width a async way, not callback. But, I don't know how to test this component.
I'd like write test code like this
it('render', async () => {
const container = mount(
<App />
);
const instance = container.instance();
console.log(instanct.state.name); // of course, state.name is undefined, but I want in this place, the state.name can be 'qyt'
});
I created a demo project on Github here.
The solution I went with was to mock the getData module:
let mockPromise;
jest.mock("./getData", () => {
return jest.fn(() => {
mockPromise = Promise.resolve({ name: "Spider man" });
return mockPromise;
});
});
Then I returned the mockPromise in my test. Jest will wait for the promise to resolve before completing the test.
it("renders without crashing", () => {
const wrapper = shallow(<App />);
return mockPromise.then((a) => {
wrapper.update();
expect(wrapper.html()).toEqual(`<h1>Name: Spider man</h1>`);
});
});
I have the following component script (some irrelevant bits removed):
import api from '#/lib/api';
export default {
methods: {
upload (formData) {
api.uploadFile(formData).then(response => {
this.$emit('input', response.data);
});
}
}
};
And I have the following test, which uses avoriaz to mount the Vue instance:
import { mount } from 'avoriaz';
import { expect } from 'chai';
import sinon from 'sinon';
import UploadForm from '#/components/UploadForm';
describe('upload', () => {
it('passes form data to api.uploadFile', () => {
const testFormData = { test: 'test' };
const api = {
uploadFile: sinon.spy()
};
const wrapper = mount(UploadForm);
wrapper.vm.api = api;
wrapper.vm.upload(testFormData);
expect(api.uploadFile.called).to.equal(true);
});
});
My sinon spy is never called, and I've tried a couple different variations on the above. What is the best way to spy on an imported function like this? Or am I conceptually approaching this the wrong way?
Problem
You need to stub the api dependency, which is a dependency of the file. This can't be done through the vue instance, since api is not a part of the vue component.
You need to stub the file dependency.
Solution
One method to do this is to use inject-loader.
Steps
Install inject-loader
npm install --save-dev inject-loader
At the top of your file, import UploadForm with inject-loader and vue-loader:
import UploadFormFactory from '!!vue-loader?inject!#/components/UploadForm';
This is a factory function that returns UploadForm with dependencies stubbed.
Now, in your test you need to call UploadFormFactory with the dependency you want stubbed:
const api = {
uploadFile: sinon.spy()
};
const UploadForm = UploadFormFactory({
'#/lib/api': api
})
So your test file will look like:
import { mount } from 'avoriaz';
import { expect } from 'chai';
import sinon from 'sinon';
import UploadFormFactory from '!!vue-loader?inject!#/components/UploadForm';
describe('upload', () => {
it('passes form data to api.uploadFile', () => {
const api = {
uploadFile: sinon.spy()
};
const UploadForm = UploadFormFactory({
'#/lib/api': api
})
const testFormData = { test: 'test' };
const api = {
uploadFile: sinon.spy()
};
const wrapper = mount(UploadForm);
wrapper.vm.upload(testFormData);
expect(api.uploadFile.called).to.equal(true);
});
});
More info
I've written a tutorial with more detail here - https://www.coding123.org/stub-dependencies-vue-unit-tests/
I think Edd's answer is the most encompassing for most scenarios, so I'm marking his as the accepted answer. However, the workaround I came up with was to make the api library a global service (Vue.prototype.$api = api) in my main.js file, and then overwrite the global with a stub before each test.
describe('UploadForm.vue', () => {
let wrapper;
const uploadFile = sinon.stub().returns(Promise.resolve({ data: 0 }));
beforeEach(() => {
wrapper = mount(UploadForm, {
globals: {
$api: { uploadFile }
}
});
});
// ...