I have the following componentDidMount method in my Overview class:
componentDidMount () {
const tick = () => {
return Overview.retrieveDevices()
.then(devices => {
this.setState({
devices: devices,
visibleDevices: devices.filter(this.searchDevices),
isLoading: false
})
this.timer = setTimeout(tick, 1000)
})
}
tick()
}
Overview.retrieveDevices() returns a promise with the data from another function that makes a fetch request to my server, the data it returns is an array.
I have the following set in my test suite:
import React from 'react'
import Overview from './Overview'
import Enzyme, { shallow, mount } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
const nock = require('nock')
Enzyme.configure({ adapter: new Adapter() })
describe('Overview.jsx', () => {
it('renders without crashing', () => {
expect(shallow(<Overview />)).toMatchSnapshot()
})
it('calls the componentDidMount function when it is created', () => {
const componentDidMountSpy = jest.spyOn(Overview.prototype, 'componentDidMount')
mount(<Overview />)
expect(componentDidMountSpy).toHaveBeenCalledTimes(1)
componentDidMountSpy.mockRestore()
})
describe('componentDidMount', () => {
let getNock
beforeEach(() => {
jest.useFakeTimers()
getNock = nock('http://localhost:3001')
.get('/api/v1/devices')
})
afterEach(() => {
jest.useRealTimers()
nock.cleanAll()
})
it('sets the devices state to an empty array when retrieve devices returns an empty array', () => {
getNock.reply(200, [])
const wrapper = mount(<Overview />)
expect(wrapper.state('devices')).toEqual([])
})
it('sets the devices state to an array of devices when retrieve devices returns devices', () => {
getNock.reply(200, [{id: '1234'}])
const wrapper = mount(<Overview />)
expect(wrapper.state('devices')).toEqual([])
jest.runOnlyPendingTimers()
wrapper.update()
expect(wrapper.state('devices')).toEqual([{id: '1234'}])
})
})
})
It is the final test that is failing... I get the following error:
expect(received).toEqual(expected)
Expected value to equal:
[{"id": "1234"}]
Received:
[]
Difference:
- Expected
+ Received
- Array [
- Object {
- "id": "1234",
- },
- ]
+ Array []
46 | jest.runOnlyPendingTimers()
47 | wrapper.update()
> 48 | expect(wrapper.state('devices')).toEqual([{id: '1234'}])
49 | })
50 | })
51 | })
at Object.it (src/components/Overview/Overview.spec.jsx:48:40)
The test to pass
Linux, Ubuntu 16.04
yarn 1.3.2
node 8.9.0
| library | version
| ---------------- | -------
| Enzyme | 3.3.0
| React | 16.2.0
The setState happens inside a promise; running all pending timers wouldn’t cause the .then to fire yet.
Even if you could get access to the promise nock returns (to wait for it) that wouldn’t give you access to the promise created by the .then, so I’m not sure what the best solution is here.
Could you move “tick” to a prototype method that returns a promise, and then you could unit-test that separately?
ahh I think I see what you mean, the promise at the point the expect is happening is still pending (please correct me if I'm wrong).... I need to wait for the promise to resolve and then run the expect? Ideally I was hoping to test the function by driving it but in this case it might not be the best idea....
I suppose given that I know it works when its running then unit tests would be void, I could possibly do end-2-end tests that could cover this.
A simple workaround could be, return Promise.resolve().then(() => { /* do your assertions here */ }); but that's brittle because it's a bit of a race condition.
I still get the following error from Jest after trying what you suggested:
Expected value to equal:
[{"id": "1234"}]
Received:
[]
Ah, you may want to do the update inside the promise too :-)
hmm still not working... this is what the test looks like now:
it('sets the devices state to an array of devices when retrieve devices returns devices', () => {
getNock.reply(200, [{id: '1234'}])
const wrapper = mount(<Overview />)
expect(wrapper.state('devices')).toEqual([])
jest.runAllTimers()
return Promise.resolve()
.then(() => {
wrapper.update()
expect(wrapper.state('devices')).toEqual([{id: '1234'}])
})
})
Could you try also spying on Overview.retrieveDevices to return a resolved promise for the proper data, and see if that will let you update the state?
If so, then it's a problem with that method interacting with nock (what's the code for that method?).
Yeah I have tried that as well so the test looks like the following:
it('sets the devices state to an array of devices when retrieve devices returns devices', () => {
const wrapper = mount(<Overview />)
let spy = jest.spyOn(Overview, 'retrieveDevices')
.mockReturnValueOnce(Promise.resolve([]))
.mockReturnValue(Promise.resolve([{id: '1234'}]))
expect(wrapper.state('devices')).toEqual([])
return Promise.resolve()
.then(() => {
jest.runAllTimers()
wrapper.update()
expect(wrapper.state('devices')).toEqual([{id: '1234'}])
spy.mockRestore()
})
})
but its the same error again, its almost like the .update() is not working....
I've also tried declaring the spy before I call mount but that didn't change anything
Does a wrapper.forceUpdate() change anything?
@ljharb afraid not, first I get wrapper.forceUpdate() is not a function... if I then code the test to have:
const component = wrapper.instance()
component.forceUpdate()
expect(component.state.devices).toEqual([{id: '1234'}])
then that fails with the same error
the only way the test will pass is if I call wrapper.setState({devices: [{id: '1234'}])
@ljharb any other ideas?
I have also run into a similar issue, and the problem is not only in componentDidMount it is in general infact, that any programmatically triggered setState changes don't take effect on UI, unless called externally in test cases.
Indeed, I don't think this is a trivially solveable problem.
could this be covered with something such as protractor (or whatever the react equivalent is) tests rather than at a unit test level?
react-testing-library is a solution for such scenarios, It is very close to DOM testing and allows one write assertions and simulations which are very similar to what we write in Selenium or Protactor.
This is still not working. Most of our state setting is done in componentDidMount. Can someone please help on this
same issue with [email protected], [email protected] and [email protected]
Same issue here. componentDidMount is calling a promise that is calling setState, however, the test still can't see the updated state.
I don't know if this is the best solution, but I also had this issue and was able to get the test to see my updated state by putting my assertions inside a setTimeout. Maybe you could try something like this?
it('sets the devices state to an array of devices when retrieve devices returns devices', async (done) => {
getNock.reply(200, [{id: '1234'}])
const wrapper = await mount(<Overview />)
setTimeout( () => {
expect(wrapper.state('devices')).toEqual([])
jest.runOnlyPendingTimers()
wrapper.update()
expect(wrapper.state('devices')).toEqual([{id: '1234'}])
done()
}, 500)
})
@jhuynh85 awaiting mount is redundant since it doesn’t return a promise; and the setTimeout is basically a race condition that you might be getting lucky and winning.
@ljharb thank you. Not ideal, but it works for me.
I think this is answered.
No framework - react itself, or any testing framework - can just "know" when your code is "finished"
because that's not how the language works. The solution is always going to be, either a) explicitly take a callback, that your test can pass; b) explicitly return a promise, that your test can await; c) mock out a part of your application to do (a) or (b). Anything else is brittle, even if it works for the moment.
I totally agree with @ljharb but if you really need to force to wait some promise, this workaround this worked for me:
....
await new Promise(res => setTimeout(() => { res() }, 0))
wrapper.update()
expect(YOUR EFFECT)...
even with the timeout to "0" it was enough to work.
That creates a race condition - if it works, you’re lucky, and it might stop working randomly.
I've inherited this from somewhere else, but we use it when we need to wait for unreachable promises.
// somewhere in your test setup code
global.flushPromises = () => {
return new Promise(resolve => setImmediate(resolve))
}
test('something with unreachable promises', () => {
expect.hasAssertions()
const component = mount(<Something />)
// do something to your component here that waits for a promise to return
return flushPromises().then(() => {
component.update() // still may be needed depending on your implementation
expect(component.html()).toMatchSnapshot()
})
})
Have you tried chaining the update() call with your other enzyme calls? I have a component that fetches data in componentDidMount() and then calls setState() with the data. The state variable is then passed as a prop to a child component, which is what I'm testing for.
I couldn't get my test to work until I chained my update() and find() calls like so:
expect(wrapper.update().find('VehiclePlate').prop('blackListNames')).toBeTruthy();
instead of
wrapper.update();
expect(wrapper.find('VehiclePlate').prop('blackListNames')).toBeTruthy();
I'm on react 16.8 and enzyme 3.10.
@jhkeung hm, those should be identical; if not i'd love an issue with repro code.
@ljharb Thanks for the call out, I went back and tweaked my test and verified it works without chaining. I'm not sure what happened the first time I wrote the tests and why it seemed to fix the async issues I was having. I also thought it was strange that it didn't work. I appreciate you monitoring this thread.
I did have to use a setTimeout in conjunction with update() in some tests I wrote yesterday. On submitting the form, the component runs various validations and has two different rounds of setState(). (I know, not ideal)
const form = mountWithTheme(<FormComponent {...props} />);
form.find('button[type="submit"]').simulate('click');
form.update();
setTimeout(() => {
expect(form.text()).toContain('Custom Field is required');
}, 0);
Most helpful comment
This is still not working. Most of our state setting is done in componentDidMount. Can someone please help on this