React v15.1.0
Jest v12.1.1
Enzyme v2.3.0
I'm trying to figure out how to test a component that calls a promise in a function invoked by a click. I was expecting Jest's runAllTicks() function to help me out here, but it doesn't seem to be executing the promise.
Component:
import React from 'react';
import Promise from 'bluebird';
function doSomethingWithAPromise() {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 50);
});
}
export default class AsyncTest extends React.Component {
constructor(props) {
super(props);
this.state = {
promiseText: '',
timeoutText: ''
};
this.setTextWithPromise = this.setTextWithPromise.bind(this);
this.setTextWithTimeout = this.setTextWithTimeout.bind(this);
}
setTextWithPromise() {
return doSomethingWithAPromise()
.then(() => {
this.setState({ promiseText: 'there is text!' });
});
}
setTextWithTimeout() {
setTimeout(() => {
this.setState({ timeoutText: 'there is text!' });
}, 50);
}
render() {
return (
<div>
<div id="promiseText">{this.state.promiseText}</div>
<button id="promiseBtn" onClick={this.setTextWithPromise}>Promise</button>
<div id="timeoutText">{this.state.timeoutText}</div>
<button id="timeoutBtn" onClick={this.setTextWithTimeout}>Timeout</button>
</div>
);
}
}
And the tests:
import AsyncTest from '../async';
import { shallow } from 'enzyme';
import React from 'react';
jest.unmock('../async');
describe('async-test.js', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<AsyncTest />);
});
// FAIL
it('displays the promise text after click of the button', () => {
wrapper.find('#promiseBtn').simulate('click');
jest.runAllTicks();
jest.runAllTimers();
wrapper.update();
expect(wrapper.find('#promiseText').text()).toEqual('there is text!');
});
// PASS
it('displays the timeout text after click of the button', () => {
wrapper.find('#timeoutBtn').simulate('click');
jest.runAllTimers();
wrapper.update();
expect(wrapper.find('#timeoutText').text()).toEqual('there is text!');
});
});
Updated answer: using async / await leads to cleaner code. Old code below.
I've successfully solved this problem by combining the following elements:
Mock out the promise and make it resolve immediately
Make the test asynchronous by marking the test function async
After simulating the click, wait until the next macrotask to give the promise time to resolve
In your example, that might look like this:
// Mock the promise we're testing
global.doSomethingWithAPromise = () => Promise.resolve();
// Note that our test is an async function
it('displays the promise text after click of the button', async () => {
wrapper.find('#promiseBtn').simulate('click');
await tick();
expect(wrapper.find('#promiseText').text()).toEqual('there is text!');
});
// Helper function returns a promise that resolves after all other promise mocks,
// even if they are chained like Promise.resolve().then(...)
// Technically: this is designed to resolve on the next macrotask
function tick() {
return new Promise(resolve => {
setTimeout(resolve, 0);
})
}
Enzyme's update() is neither sufficient nor needed when using this method, because Promises never resolve in the same tick they are created -- by design. For a very detailed explanation of what is going on here, see this question.
Original answer: same logic but slightly less pretty. Use Node's setImmediate to defer the test until the next tick, which is when the promise will resolve. Then call Jest's done to finish the test asynchronously.
global.doSomethingWithAPromise = () => Promise.resolve({});
it('displays the promise text after click of the button', (done) => {
wrapper.find('#promiseBtn').simulate('click');
setImmediate( () => {
expect(wrapper.find('#promiseText').text()).toEqual('there is text!');
done();
})
});
This isn't as nice because you'll get big nested callbacks if you have to wait for more than one promise.
There isn't much around needing to somehow wait for the promise to fulfill before ending the test. There are two main ways of doing it from your code that I can see.
independently test that onClick and your promise methods. So check that onClick calls the correct function, but spying on setTextWithPromise, triggering a click and asserting that setTextWithPromise was called. Then you can also get the component instance and call that method which returns the promise you can attach a handler and assert it did the right thing.
expose a callback prop that you can pass in that is called when the promise resolves.
Related
I have a function like this:
join(): void {
this.working.value = true;
if (this.info.value) {
axios.get('/url')
.then((result: ResultStatus) => {
this.result = result;
})
.catch((reason: AxiosError) => {
this.showError(AjaxParseError(reason));
})
.finally(() => {
this.working.value = false;
});
}
}
and I want to write some unit tests for this. The first unit test I want to write is to test that 'this.saving' is set to true so that I ensure my UI has a value it can use to show a loading indicator.
However, when I use jest to mock axios, jest resolves the axios promise immediately and I don't have a chance to test what happens before the then/finally block is called. Here is what my unit test code looks like:
import axios from 'axios';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
import successResponse from './__json__/LeagueJoinInfoSuccess.json';
describe('constructor:', () => {
let vm: classUnderTest;
beforeEach(() => {
vm = new classUnderTest();
mockedAxios.get.mockResolvedValue({ data: successResponse }); // set up the response
vm.join(); // the function under test
});
it('should set working true before calling the server to join', () => {
expect(vm.working.value).toBeTruthy();
});
it('should set working false after calling the server responds', async () => {
await flushPromises();
expect(vm.working.value).toBeFalsy();
});
});
The first expect statement is always false because the finally block is run before I have a chance to do an await flushPromises(); so the working value is always false.
Is there a convenient way to get jest's mock of axios to wait before resolving its promise?
UPDATE: Now here is a really strange thing: If I move the contents of BeforeEach into each of the tests, then it behaves the way that I am hoping it would behave. I guess I will open an issue over at jest and ask them what's going on.
I have a solution for you to create a promise as response, however, we're not gonna resolve it in the 1st test case to keep you test loading state then resolve it in the 2nd test as following:
describe('constructor:', () => {
let vm: classUnderTest;
// Resolve function
let resolveFn: (value?: unknown) => void
beforeEach(() => {
vm = new classUnderTest();
const promise = new Promise(resolve => {
// hold the resolve function to call in 2nd test
resolveFn = resolve;
});
mockedAxios.get.mockImplementation(() => promise);
vm.join(); // the function under test
});
it('should set working true before calling the server to join', () => {
expect(vm.working.value).toBeTruthy();
});
it('should set working false after calling the server responds', async () => {
// resolve the pending promise
resolve({
data: successResponse,
});
// I think you would wait the promise resolved
await new Promise(resolve => setTimeout(resolve));
expect(vm.working.value).toBeFalsy();
});
});
I have a problem that I can't understand and I was hoping that someone could help me with.
This is my test: state.messages is an empty array and api.botReply is called 0 times when it is in the function to be ran.
state.typing is set to true so I know I run the function.
test('test to resolve data from botReply', done => {
const wrapper = shallow(<Bot />);
api.botReply = jest.fn(() =>
Promise.resolve(wrapper.setState({ typing: false }))
);
wrapper.instance().sendReply();
setImmediate(() => {
wrapper.update();
console.log(wrapper.state('typing'));
console.log(wrapper.state('messages'));
expect(api.botReply).toHaveBeenCalledTimes(1);
done();
});
});
And this is the function that is run:
sendReply = () => {
this.setState({ typing: true });
api.botReply()
.then(reply => {
this.setState({ messages: [...this.state.messages, reply], typing: false });
})
};
Discarding promise chains and using random delays can lead to race conditions like this one.
Since a promise is provided in tests, it should be chained to maintain correct control flow. It's not a good practice to assign Jest spies as methods because they won't be cleaned up afterwards. A promise is supposed to resolve with reply, not set state.
It should be something like:
test('test to resolve data from botReply', async () => {
const wrapper = shallow(<Bot />);
const promise = Promise.resolve('reply')'
jest.spyOn(api, 'botReply').mockImplementation(() => promise);
wrapper.instance().sendReply();
expect(wrapper.state('typing')).toBe(true);
await promise;
expect(api.botReply).toHaveBeenCalledTimes(1);
expect(wrapper.state('typing')).toBe(false);
});
My react component runs an asynchronous query to obtain some data using lodash debounce - since user input can cause re-queries and I want to rate-limit the queries - and then sets the state of the component with the results that are returned.
MyComponent (React Component)
componentWillMount() {
this.runQuery();
}
handler = (response) => {
this.setState({ results: response.results });
}
runQuery = _.debounce((props = this.props) => {
// run the query
doStuff(mainParams)
.withSomeOtherStuff(moreParams)
.query()
.then(this.handler)
.catch((error) => {
this.props.catchError(error);
});
}, 200);
I am currently stubbing out my main api exit point that goes out and fetches the data which returns a promise thanks to the sinon-stub-promise package
before((done) => {
stub = stubGlobalFn('evaluate'); // returns stubbed promise, uses npm:sinon-stub-promise
});
This allows me the ability to use my custom Reader (tested elsewhere) to read in a mock response and then resolve it synchronously for testing purposes.
mytest.spec.jsx
let stub;
const testWithProps = props => (
new Promise((resolve, reject) => {
new Reader(histories).readGrid((err, grid) => {
try {
expect(err).to.be.a('null');
stub.resolves(grid);
// ....
Then in the same testWithProps function I'm able to mount the Table component with the props I specify in my test as sort of a test factory.
const wrapper = mount(<Table {...props} />);
And here's where I run into my confusion, I have stubbed out the promise that gets resolved when the main evaluate async function is invoked but not the state handler.
stub.thenable.then(() => {
// --------------------------
// PROBLEM: how to test without setting a timeout?
// --------------------------
setTimeout(() => {
resolve(wrapper.update());
}, 200);
// --------------------------
// --------------------------
});
Should I be stubbing my handler function inside of my react component instead if I'm wanting to test the state of the component after the async behavior? I'm not sure how to even stub that part out if that's even what's needed.
Ultimately my test looks like this by the end:
it('toggles the row for the value when clicked', () => {
const props = {
// some props that I use
};
return testWithProps(props).then((wrapper) => {
// simply testing that my mocked response made it in successfully to the rendered component
expect(wrapper.state().results.length).to.equal(4);
});
});
I have a promise that return once a correct event is called with the correct action. This is what I have so far
import {EventBus} from "./EventBus";
export function completed() {
EventBus.$on('queue-action', e => {
return new Promise((resolve,reject) => {
if(e.action == 'completed'){
let item = e.queueItem
resolve(item);
}else{
reject(new Error('No action specified in event object'))
}
})
});
}
export function emitAction(action, queueItem) {
EventBus.$emit('queue-action', {
action,
queueItem
});
}
When calling the completed function in one of my components like this
completed()
.then((item)=> console.log('promise'))
.catch((error) => console.log(error) );
it returns undefined once I add the then and catch methods to this function. It looks like the problem is with me then and catch, but I am unable to determine what it is. From what I have seen online whatever variable you use for the data you use in the then statement.
What I am trying to do is let an element in the "queue" to emit an event to the to the queue with an action for example completed. The queue should then resolve the promise to edit the queue in the intended purpose of that action or react to an error from the promise.
This is what I have done so far
import {EventBus} from "./EventBus";
export class QueueEvent {
constructor(){}
emitAction(action, queueItem){
return new Promise((resolve,reject) => {
EventBus.$emit('queue-action', {
action,
queueItem
},resolve,reject);
});
}
}
export class QueueEvents extends QueueEvent{
constructor(){
super();
}
listenForComplete() {
}
}
Your completed function is not returning a promise (it is returning undefined as you noticed).
You are returning the promise for the event emitter when the queue-action is called. You are defining a new function here: e => { and that function that is returning a promise is passed to the EventBus event emitter
You want to wrap the whole EventBus.$on() in your promise, like this:
export function completed() {
return new Promise((resolve) => {
EventBus.$on('queue-action', e => {
if(e.action == 'completed'){
let item = e.queueItem
resolve(item);
}
});
});
}
As a rule of thumb, unless you have a very specific reason to do something else, a function returning a promise should have all it's body wrapped in return new Promise(...);. It is also normal and ok to have a lot of code wrapped inside a promise.
Note to the code: I removed reject part both for brevity and because I'm not sure that is what you want to do. Unless it is an error if some action happens before 'completed', you should just ignore such an event.
I am trying to test that a function inside a stubbed promise was called. Unfortunately the spy on that function seems to be called only after the tests. Here is a simplified version of my React code.
export default class TheComponent extends React.Component {
constructor(props) {
super(props);
}
onSend(data) {
AUtility.aPromise(data).then(() => {
this.props.onSend(data);
console.log("Props onSend was called!")
});
}
render() {
return null;
}
}
and here is the test I try to run.
it('should call the props function', (done) => {
const onSendSpy = spy();
const promiseStub = stub(AUtility, 'aPromise').resolves('mock string');
wrapper = shallow(<TheComponent onSend={onSendSpy} />
wrapper.instance().onSend();
expect(promiseStub.calledOnce).to.be.true; //this passes
expect(onSendSpy.calledOnce) //this fails
done();
});
Whenever I run the test the first assertion passes but not the second. However the print statement in my code still prints the string during the test. It seems that the done() function will make the test finish once the promise returns and not once the then block is fully executed. How can I get this test to pass?
I'm by no means an expert, but could you try wrapping the two expect statements in a setTimeout function?
setTimeout(() => {
expect(promiseStub.calledOnce).to.be.true;
expect(onSendSpy.calledOnce);
done(); }, 0);