React Jest Test Fails when setState Called in Promise - javascript

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

Related

Testing inner logic of method using vue-test-utils with Jest

How can I test the inner logic of the following method?
For example:
async method () {
this.isLoading = true;
await this.GET_OFFERS();
this.isLoading = false;
this.router.push("/somewhere");
}
So I have the method that toggles isLoading, calls an action, and routes somewhere. How can I be sure that isLoading was toggled correctly (true before action call and false after)?
You have to extract this.isLoading rows into a new method setLoading() and check if it was called.
The second argument of shallowMount/mount is the mounting options that could be used to override the component's data props upon mounting. This lets you pass in a setter that mocks the isLoading data prop, which then allows you to verify the property was modified within the method under test:
it('sets isLoading', () => {
const isLoadingSetter = jest.fn()
const wrapper = shallowMount(MyComponent, {
data() {
return {
// This setter is called for `this.isLoading = value` in the component.
set isLoading(value) {
isLoadingSetter(value)
}
}
}
})
//...
})
Then, you could use toHaveBeenCalledTimes() along with isLoadingSetter.mock.calls[] to examine the arguments of each call to the mocked setter. And since you want to test the effects of the async method, you'll have to await the method call before making any assertions:
it('sets isLoading', async () => {
//...
await wrapper.vm.method()
expect(isLoadingSetter).toHaveBeenCalledTimes(2)
expect(isLoadingSetter.mock.calls.[0][0]).toBe(true)
expect(isLoadingSetter.mock.calls.[1][0]).toBe(false)
})
If you also want to verify that GET_OFFERS() is called, you could use jest.spyOn() on the component's method before mounting:
it('gets offers', async () => {
const getOfferSpy = jest.spyOn(MyComponent.methods, 'GET_OFFERS')
const wrapper = shallowMount(MyComponent, /*...*/)
await wrapper.vm.method()
expect(getOfferSpy).toHaveBeenCalledTimes(1)
})

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.

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

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

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

this.setState not working on componentWillMount()?

I want the state to be dependent on server data. I thought of using componentWillMount:
componentWillMount() {
this.setState( async ({getPublicTodosLength}, props) => {
const result = await this.getPublicTodosLengthForPagination();
console.log("result = ", result) // returns the length but not assigned on this.state.getPublicTodosLength
return { getPublicTodosLength: result+getPublicTodosLength }
});
}
getPublicTodosLengthForPagination = async () => { // get publicTodos length since we cannot get it declared on createPaginationContainer
const getPublicTodosLengthQueryText = `
query TodoListHomeQuery {# filename+Query
viewer {
publicTodos {
edges {
node {
id
}
}
}
}
}`
const getPublicTodosLengthQuery = { text: getPublicTodosLengthQueryText }
const result = await this.props.relay.environment._network.fetch(getPublicTodosLengthQuery, {})
return await result.data.viewer.publicTodos.edges.length;
}
There is value but it's not assigned on my getPublicTodosLength state? I think I don't have to bind here since result returns the data I wanted to assign on getPublicTodosLength state
Why not rather do something like this?
...
async componentWillMount() {
const getPublicTodosLength = this.state.getPublicTodosLength;
const result = await this.getPublicTodosLengthForPagination();
this.setState({
getPublicTodosLength: result+getPublicTodosLength,
});
}
...
It's simpler and easier to read. I think the problem with the original code is with using async function inside setState(). In transpiled code there is another wrapper function created and then it probably loose context.
If you want your state to be dependent on server data you should use componentDidMount().
componentWillMount() is invoked immediately before mounting occurs. It is called before render(), therefore setting state synchronously in this method will not trigger a re-rendering. Avoid introducing any side-effects or subscriptions in this method.
This is the only lifecycle hook called on server rendering. Generally, we recommend using the constructor() instead.
componentDidMount() is invoked immediately after a component is mounted. Initialization that requires DOM nodes should go here. If you need to load data from a remote endpoint, this is a good place to instantiate the network request. Setting state in this method will trigger a re-rendering.
From React Doc
May be you could use:
Code snippet:
(async ({getPublicTodosLength}, props) => {
const result = await this.getPublicTodosLengthForPagination();
console.log("result = ", result);
return { getPublicTodosLength: result + getPublicTodosLength }
})).then((v)=> this.setState(v));
Please let me know if that works.
i decided to make componentWillMount async and it worked well.
this is the code:
componentWillMount = async () => {
let result = await this.getPublicTodosLengthForPagination();
this.setState((prevState, props) => {
return {
getPublicTodosLength: result
}
});
}

Categories

Resources