Enzyme: on simulating "click", test asynchronously changed state value - javascript

I am trying to test a state value (say: state.name) which asynchronously changes on a button click.
Initially
state.nameis "Random Value"
When a button is clicked state.name changes to "peter" from "Random Value"
works as expected in browser
Need it to work the same on button.simulate('click') in my test
My component App.js:
import React, { Component } from 'react';
class App extends Component {
state = {name: "Random Value"};
render() {
return (
<div className="App">
<div>{this.state.name}</div>
<button onClick={this.handleClick}>GetData</button>
</div>
);
}
handleClick = () => {
const currentContext = this;
fetch('http://api-call/getdata')
.then(function(response) {
return response.json();
})
.then(function(jsonData) {
// jsonData.name = "peter"
currentContext.setState({name: jsonData.name});
})
}
}
export default App;
The test file App.test.js
describe('<App/>', () => {
it('should handle click correctly', () => {
const wrapper = shallow(<App />);
expect(wrapper.find('button').length).toBe(1);
expect(wrapper.state().name).toEqual("Random Value");
wrapper.find('button').simulate('click');
expect(wrapper.update().state().name).toEqual("peter");
});
});
Result:
// FAILED!
Expected value to equal:
"peter"
Received:
"Random Value"
What else have I tried?
All sorts of solutions out there like using async await, setImmediate and then wrapper.update() and many other things. Maybe I did something wrong or missed something. Anyways I have spent an evening trying to do it. I need help from enzyme experts.
Thanks

First you need mock fetch somehow. Sending real request not only breaks isolation of unit-tests and adds risks of inconsistency. It's also harder to wait for response when you don't know when it may finish. There are different ways to achieve that. jest-fetch-mock is one of them.
Also I advice you don't check for state but rather check for render() results.
function getName(wrapper) {
return wrapper.find('.App > div').at(0).props().children;
}
it('should handle click correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({ name: '12345' }));
const wrapper = shallow(<App />);
expect(wrapper.find('button').length).toBe(1);
expect(getName(wrapper)).toEqual("Random Value");
wrapper.find('button').simulate('click');
await Promise.resolve();
expect(getName(wrapper)).toEqual("peter");
});
What's going on here. await Promise.resolve() is just just waits until all promises already resolved are run. It means without that our mocked response will not run between button click and expect runs.
Another way to get positive result is making handleClick() to return Promise we can await for:
...
handleClick = () => {
const currentContext = this;
return fetch('http://api-call/getdata')
.then(function(response) {
return response.json();
})
.then(function(jsonData) {
// jsonData.name = "peter"
currentContext.setState({name: jsonData.name});
})
}
....
it('should handle click correctly', async () => {
....
expect(getName(wrapper)).toEqual("Random Value");
await wrapper.find('button').simulate('click');
expect(getName(wrapper)).toEqual("peter");
});
or without async-await syntax:
it('should handle click correctly', () => {
....
expect(getName(wrapper)).toEqual("Random Value");
return wrapper.find('button').simulate('click').then(() => {
expect(getName(wrapper)).toEqual("peter");
});
});
But I really don't like this way since event handler have to return a Promise that it typically does not do.
More on microtasks(Promises)/macrotasks(timers, events) yuo may read here: https://abc.danch.me/microtasks-macrotasks-more-on-the-event-loop-881557d7af6f
More on testing async code in jest you better check their docs: https://jestjs.io/docs/en/asynchronous

Related

Waiting for React component state to update before testing with Jest

I have a component with a handleAdd function. This function calls a library, which in turn calls axios and returns a promise. Once that's resolved, the handleAdd() method updates component state which in turns renders child(ren).
In other words, it checks with the server first to make sure the item is added before displaying it locally.
When testing with Jest, i have to await sleep for a few msec before the expect runs otherwise the shallow render isn't updated yet, even though I mock/overwrite the api call. There's some delay between the promise resolving, rerender and expect(). Here's what that kind of looks like:
it('adds a thing', async () => {
ThingManager.default.addPlan = () => {
const response = new Promise((resolve, reject) => { resolve() })
return response;
}
const wrapper = shallow(<Home />)
wrapper.find('button').simulate('click')
const input = wrapper.find('#plan-title')
input.simulate('change', { target: { value: 'TEST ITEM' } })
await sleep(500) // without it, <Thing /> isn't rendered yet.
expect(wrapper.find('Thing').length).toBe(1)
});
What's the proper way of doing this?
Just wanted to throw it out there that I use simple setTimeout with the combination of jest's done().
EDIT
it('sample test case', (done) => {
// initialize your component
setTimeout(function () {
// expect something that's available after the timeout
done();
}, 500);
});
You can use act from test-utils. That is what the React docs recommend, but I have had more success with waitFor from testing-library.

Testing async componentDidMount() with react-test-renderer

I have a component which does some SQLite loads in it's componentDidMount() function
async componentDidMount() {
try {
const user = await this.userDao.getUserData();
const setupNeeded = user === null;
if( setupNeeded ) {
this.setState({
status : 'setup'
});
}
else {
this.setState({
status : 'ready',
seed: user.seed
})
}
} catch (e) {
logger.error("Error during db query", e);
this.setState({
status: 'corrupted'
});
}
}
And I would like to test the rendered result after the call to getUserData() has been resolved, and the state has been set accordingly.
Now in my test, I have the actual call to the database mocked away, so the Promise should be resolved immediately, yet testing like this does not work as expected:
Initially, I tried it like this:
test('Should render SetNewPasswordScreen', async () => {
const tree = await renderer.create(<IndexScreen/>);
const json = tree.toJSON();
// expect stuff
});
However the json in this case contains the data of the initial render() call.`
Doing it like this will work:
test('Should render SetNewPasswordScreen', (done) => {
const tree = renderer.create(<IndexScreen/>);
setTimeout(() => {
const json = tree.toJSON();
// expect stuff
}, 5000);
});
But this is not ideal, because I am just guessing that after 5 seconds everything will be done, but I don't know. Also, it's less then suitable if a test takes 5 seconds to finish. (I used 5 seconds arbitrarily, probably it will also work with much less since the async call is mocked anyway, but I can never really know)
My question is if anybody has a better idea of how to solve this issue?
it('should render setNewPassWordScreen', async () => {
const tree = await renderer.create(<IndexScreen/>);
const instance = tree.getInstance();
await instance.componentDidMount();
// expect other stuff.
});
Little late for the party but faced the issues as well. Here is a handy shortcut function I use with the adviced usage of act:
import renderer from "react-test-renderer"
import { ReactElement } from "react"
import { act } from "#testing-library/react-native"
export const expectToMatchSnapshot = async (component: ReactElement) => {
let tree
await act(async () => {
tree = renderer.create(component)
})
expect(tree.toJSON()).toMatchSnapshot()
}
In your case you should only then add:
test("should render ***", async () => {
await expectToMatchSnapshot(<IndexScreen/>)
}
you could use the wait for expect package:
https://www.npmjs.com/package/wait-for-expect
test('Should render SetNewPasswordScreen', async (done) => {
const tree = renderer.create(<IndexScreen/>);
await waitForExpect(() => {
const json = tree.toJSON();
// expect stuff
});
});
mind the function passed into the test('...', fn) is now an async function.

How to verify console.log was called in componentDidMount using Jest and Enzyme?

I'm trying test for cases when my axios call does not get an HTTP response of 200. When axios does not get a successful response, it throws an error. I want to verify that console.log gets called twice in this case.
Here's a snippet of the class I'm testing:
class App extends React.Component {
...
async componentDidMount() {
let url = "/api/info/tmp"
try {
let response = await axios.get(url);
...do stuff
this.setState(...);
} catch (e) {
console.log("Could not get " + url);
console.log(e);
}
}
...
}
And here's a snippet of my jest test
let mockAxios = new MockAdapter(axios);
...
describe("App - componentDidMount() behavior test", () => {
beforeEach(() => {
app = shallow(<App />);
})
afterEach(() => {
app = undefined;
mockAxios.reset();
});
...
describe("Get " + url + " HTTP response status is not 200", () => {
beforeAll(() => {
mockAxios.onGet(url).reply(302, mockData);
});
it("Does not set state regardless of response body", () => {
console.log = jest.fn();
const state = app.state();
expect(console.log).toHaveBeenCalledTimes(2);
expect(state.solutions).toEqual({});
expect(state.username).toEqual("");
});
});
});
I know the console.log = jest.fn() bit is doing something because the console does not log the fake error anymore when I set it. However, the test fails because Expected mock function to have been called two times, but it was called zero times.
I've tried moving the console.log = jest.fn() into the "beforeEach", "beforeAll", and as a global variable.
UPDATE
I am pretty sure it's something to do with all the async that is going on.
If I do this:
it("Does not set state regardless of response body", async () => {
console.log = jest.fn();
await app.instance().componentDidMount();
expect(console.log).toHaveBeenCalledTimes(2);
const state = app.state();
expect(state.solutions).toEqual({});
expect(state.username).toEqual("");
});
Then the test still fails but my reason changed: Expected mock function to have been called two times, but it was called four times. Now I just got to figure out why it was called four times not twice.
UPDATE 2
I figured out why console.log was being called 4 times! Now I just need to figure out how I should refactor my tests.
If I comment out my jest mock, and even the whole unit test
it("Does not set state regardless of response body", async () => {
//const state = app.state();
//expect(state.solutions).toEqual({});
//expect(state.username).toEqual("");
//expect(console.log).toHaveBeenCalledTimes(2);
});
Then I can count in my console that there are already indeed two different console.log calls. shallow(<App />) must be already calling componentDidMount() or something. When I add app.instance().componentDidMount(), I can visually see that it is logging 4 times.
Updated Answer
Since it looks like you already know what you're doing with mocks, perhaps the issue has to do with componentDidMount().
I believe that your call to shallow(<App />) will already call App's componentDidMount() one time (which means your console.log will get called twice there).
Then, you subsequently call app.instance().componentDidMount() - that is, you call componentDidMount() again (which means your console.log will get called twice there again).
So, total... 4 calls to console.log.
Hope that points you in the right direction...
Original Answer
Actually, your question looks quite similar to [this StackOverFlow question on how to "How to mock console when it is used by a third-party library?"
You can use Jest mock functions to spyOn the global.console object.
For example, your test may look like this:
// Setup jest to spy on the console
const consoleSpy = jest.spyOn(global.console, 'log')
describe('App - componentDidMount() behavior test', () => {
beforeEach(() => {
jest.resetAllMocks() // reset your consoleSpy state back to initial
app = shallow(<App />)
})
...
it('Does not set state regardless of response body', () => {
const spy = jest.spyOn(global.console, 'log')
const state = app.state()
expect(consoleSpy).toHaveBeenCalledTimes(2)
expect(state.solutions).toEqual({})
expect(state.username).toEqual('')
})
...
Ideally, you'd move your API call outside of componentDidMount and into its own class method. Thay way it can be manually invoked from a lifecycle method or from an event callback. Also, you should anticipate the response to affect your UI state in some fashion (example: displaying a message to the user that the request failed and to try again).
The following example can be done with .then/.catch instead of async/await. Either way, you're working with Promises that are asynchronous and therefore they need asynchronous tests.
Note: The below assumes disableLifecycleMethods is true in the enzyme adapter. Also, just testing state changes (or a console.log) is a bit superfluous; instead, you would test if a component is rendered based upon the current state.
Working example: https://codesandbox.io/s/939w229l9r (includes both end to end and integration tests --- you can run the tests by clicking on the Tests tab located near the bottom left of the sandbox)
App.js (this will be a container that holds all relevant state and disperses it to its children as needed)
import React, { Component } from 'react';
class App extends Component {
state = = {
error: "",
isLoading: true,
solutions: {},
username: ""
};
componentDidMount() {
this.fetchData("/api/info/tmp");
}
fetchData = async (url) => {
try {
const res = await axios.get(url);
...do stuff
this.setState({
error: "",
isLoading: false,
solutions: res.data.solutions,
username: res.data.username
});
} catch (err) {
this.setState({
error: err,
isLoading: false,
solutions: {},
username: ""
});
}
}
render() { ... }
}
App.test.js (this assumes you'd want an end to end test)
import { shallow } from 'enzyme';
import App from './App';
const timeout = () =>
new Promise(resolve => {
setTimeout(() => {
resolve();
}, 2000);
});
const initialState = {
error: "",
isLoading: true,
solutions: {},
username: ""
};
describe("App", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<App />);
wrapper.setState({ ...initialState });
});
afterAll(() => {
wrapper.unmount();
});
it("sets data to state based upon successful API call", async () => {
wrapper.instance().fetchData("/api/info/tmp");
await timeout();
wrapper.update();
expect(wrapper.state('isLoading')).toBeFalsy();
expect(wrapper.state('solutions')).toEqual({ somedata });
expect(wrapper.state('username')).toEqual("Some User");
});
it("displays an error upon unsuccessful API call", async () => {
wrapper.instance().fetchData("/api/bad/url");
await timeout();
wrapper.update();
expect(wrapper.state('isLoading')).toBeFalsy();
expect(wrapper.state('solutions')).toEqual({});
expect(wrapper.state('username')).toEqual("");
expect(wrapper.state('error')).toEqual("No data found.");
});
});
App.test.js (this assumes you'd want an integration test)
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import React from "react";
import { shallow } from "enzyme";
import App from "../App";
const solutions = [{ ... }, { ... }];
const username = "Some User"
const mockAxios = new MockAdapter(axios);
const initialState = {
error: "",
isLoading: true,
solutions: {},
username: ""
};
describe("App", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<App />);
wrapper.setState({ ...initialState });
});
afterEach(() => {
mock.reset();
});
afterAll(() => {
mock.restore();
wrapper.unmount();
});
it("displays an error upon unsuccessful API call", async () => {
try {
mockAxios.onGet("/users").networkErrorOnce();
await axios.get("users");
} catch (err) {
const error = err.toString();
wrapper.setState({
error,
isLoading: false,
solutions: {},
username: ""
});
wrapper.update();
expect(wrapper.state('isLoading')).toBeEqual(error);
expect(wrapper.state('isLoading')).toBeFalsy();
expect(wrapper.state('solutions')).toEqual({});
expect(wrapper.state('username')).toEqual("");
}
});
it("sets data to state based upon successful API call", async () => {
try {
mockAxios.onGet("/users").reply(200, { solutions, username });
const res = await axios.get("users");
wrapper.setState({
error: "",
isLoading: true,
solutions: res.data.solutions,
username: res.data.username
});
wrapper.update();
expect(wrapper.state('isLoading')).toBeFalsy();
expect(wrapper.state('solutions')).toEqual(solutions);
expect(wrapper.state('username')).toEqual(username);
} catch (e) {
console.log(e);
}
});
});
I figured it out! Kind of... I am not certain why it works like this, but setting the mock in the actual "it" did not work.
The solution was making a beforeEach and afterEach
describe("Get " + url + " HTTP response status is not 200", () => {
beforeAll(() => {
mockAxios.onGet(url).reply(302, mockData);
});
beforeEach(() => {
console.log = jest.fn();
});
afterEach(() => {
jest.resetAllMocks();
});
it("Does not set state regardless of response body", async () => {
const state = app.state();
expect(state.solutions).toEqual({});
expect(state.username).toEqual("");
expect(console.log).toHaveBeenCalledTimes(2);
});
});

Wait for fetch() to resolve using Jest for React test?

In my componentDidMount of a React.Component instance I have a fetch() call that on response calls setState.
I can mock out the request and respond using sinon but I don't know when fetch will have resolved it's promise chain.
componentDidMount() {
fetch(new Request('/blah'))
.then((response) => {
setState(() => {
return newState;
};
});
}
In my test using jest with enzyme:
it('has new behaviour from result of set state', () => {
let component = mount(<Component />);
requests.pop().respond(200);
component.update() // fetch() has not responded yet and
// thus setState has not been called yet
// so does nothing
assertNewBehaviour(); // fails
// now setState occurs after fetch() responds sometime after
});
Do I need to flush the Promise queue/callback queue or something similar? I could do a repeated check for newBehaviour with a timeout but that's less than ideal.
The best answer it seems is to be use a container pattern and pass down the API data from a container class with separated concerns and test the components separately. This allows the component under test to simply take the API data as props and makes it much more testable.
Since you're not making any real api calls or other time-consuming operations, the asynchronous operation will resolve in a predictably short time.
You can therefore simply wait a while.
it('has new behaviour from result of set state', (done) => {
let component = mount(<Component />);
requests.pop().respond(200);
setTimeout(() => {
try {
component.update();
assertNewBehaviour();
done();
} catch (error) {
done(error);
}
}, 1000);
});
The react testing library has a waitFor function that works perfectly for this case scenario.
I will give an example with hooks and function as that is the current react pattern. Lets say you have a component similar to this one:
export function TestingComponent(props: Props) {
const [banners, setBanners] = useState<MyType>([]);
React.useEffect(() => {
const response = await get("/...");
setBanners(response.banners);
}, []);
return (
{banners.length > 0 ? <Component> : </NoComponent>}
);
}
Now you can write a test like this to make sure that when banners are set Component is rendered
test("when the banner matches the url it renders", async () => {
const {container} = render(<TestingComponent />);
await waitFor(() => {expect(...).toBe(...)});
});
waitFor will wait for the condition in the function to be met before proceeding. There is a timeout that will fail the test if the condition is not met in X time. Check out the react testing library docs for more info

React Jest Test Fails when setState Called in Promise

I'm trying to mock out the a service that returns promises so that I can verify it gets called with the correct parameters. The way the service is called varies based on the state and the first call to the service sets the state.
When setting the state in the promise it is not updating unless I wrap the assertion in setTimeout or completely stub out the promise. Is there a way to do this with just a plain promise and an expect?
My component:
class App extends Component {
constructor(props) {
super(props);
this.state = {results: []};
this.service = props.service;
this.load = this.load.bind(this);
}
load() {
if (this.state.results.length === 0) {
this.service.load('state is empty')
.then(result => this.setState({results: result.data}));
} else {
this.service.load('state is nonempty')
.then(result => this.setState({results: result.data}));
}
}
render() {
return (
<div className="App">
<button id="submit" onClick={this.load}/>
</div>
);
}
}
My test:
it('Calls service differently based on results', () => {
const mockLoad = jest.fn((text) => {
return new Promise((resolve, reject) => {
resolve({data: [1, 2]});
});
});
const serviceStub = {load: mockLoad};
let component = mount(<App service={serviceStub}/>);
let button = component.find("#submit");
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is empty');
button.simulate('click');
//this assertion fails as the state has not updated and is still 'state is empty'
expect(mockLoad).toBeCalledWith('state is nonempty');
});
As mentioned, the following works, but I'd rather not wrap the expect if there's a way around it:
setTimeout(() => {
expect(mockLoad).toBeCalledWith('state is nonempty');
done();
}, 50);
I can also change how I mock the function to stub out the promise which will work:
const mockLoad = jest.fn((text) => {
return {
then: function (callback) {
return callback({
data : [1, 2]
})
}
}
});
But I'd like to just return a promise.
React batches setState calls for performance reasons, so at this point
expect(mockLoad).toBeCalledWith('state is nonempty');
the condition
if (this.state.results.length === 0) {
is most likely still true, because data has not yet been added to state.
Your best bets here are
Either use forceUpdate between the first and second click event.
Or split the test into two separate, while extracting common logic outside of the test. Even the it clause will become more descriptive, for instance: it('calls service correctly when state is empty') for the first test, and similar for the second one.
I'd favour the second approach.
setState() does not always immediately update the component. It may batch or defer the update until later.
Read more here.
Using Sinon with Sinon Stub Promise I was able to get this to work. The stub promise library removes the async aspects of the promise, which means that state gets updated in time for the render:
const sinon = require('sinon');
const sinonStubPromise = require('sinon-stub-promise');
sinonStubPromise(sinon);
it('Calls service differently based on results', () => {
const mockLoad = jest.fn((text) => {
return sinon.stub().returnsPromise().resolves({data: [1, 2]})();
});
const serviceStub = {load: mockLoad};
let component = mount(<App service={serviceStub}/>);
let button = component.find("#submit");
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is empty');
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is nonempty');
});
See:
http://sinonjs.org/
https://github.com/substantial/sinon-stub-promise

Categories

Resources