-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Testing component with async componentDidMount #1587
Comments
@kazagkazag What about this? test("should render error if initial data is NOT available", () => {
const mounted = mount(
<App
getUserData={rejectPromise}
getAppData={resolvePromise}
/>
);
return Promise
.resolve(mounted)
.then(() => {})
.then(() => {
mounted.update()
expect(mounted.text()).toContain("Error");
});
}); |
It doesn't work either. But: return Promise
.resolve(mounted)
.then(() => mounted.update())
.then(() => mounted.update())
.then(() => {
expect(mounted.text()).toContain("Error");
}); works. (notice second update()) I don't know why, I have to investigate that... This also happens to work: test("should render error if initial data is NOT available", (done) => {
const mounted = mount(
<App
getUserData={rejectPromise}
getAppData={resolvePromise}
/>
);
setImmediate(() => {
expect(mounted.text()).toContain("Error");
done();
});
}); I don't know If I can rely on that tests... |
@kazagkazag Promise.resolve()
.then(() => console.log('1'))
.then(() => console.log('2'))
Promise.resolve()
.then(() => console.log('3'))
.then(() => console.log('4')) Also, the following prints Promise.reject()
.then(() => console.log('1'))
.catch(() => console.log('2'))
Promise.resolve()
.then(() => console.log('3'))
.then(() => console.log('4')) Even if Promise is already resolved, Promise isn't processed synchronously. In your case, return Promise
.all([
this.props.getUserData(),
this.props.getAppData()
])
.then(() => {
this.setState({
initialized: true
});
})
.catch((error) => {
this.setState({
error: true
});
}); When If you use |
Ok, thanks for explanation, but I have one more question:
Why promise is processed through .then() ? If any of those promises reject it should skip to .catch() section, shouldn't it? Moreover, |
@kazagkazag Because You can try it. Promise.resolve().then().then().then(() => console.log('1'));
Promise.resolve().then().then(() => console.log('2'));
Promise.resolve().then(() => console.log('3')); |
One hacky solution for now, is to flush all promises, as explained here: |
That isn't a valid solution. The proper solution is, whatever you're doing in your code that creates a promise - you'll have to get access to that promise in tests so that you can wait until it's resolved. |
Problem is that Two others possible solutions:
class App extends Component {
busy;
componentDidMount() {
this.busy = callApi().then(state => this.setState(state));
}
}
it('should work', async () => {
callApi.mockReturnValue(Promise.resolve(...));
wrapper = shallow(<App />);
await wrapper.instance().busy;
// state should be updated at this moment
});
class App extends Component {
componentDidMount() {
callApi().then(state => this.setState(state));
}
}
it('should work', async () => {
callApi.mockReturnValue(Promise.resolve(...));
wrapper = shallow(<App />);
await Promise.resolve();
// state should be updated at this moment
}); |
@m-architek interesting result. Could you share to us why the second example ( This is result of what I tried in Chrome console // notice that no promises is returned
const runPromises = () => {
Promise.resolve().then().then().then(() => console.log('1'));
Promise.resolve().then().then(() => console.log('2'));
Promise.resolve().then(() => console.log('3'));
Promise.reject().then(() => console.log('success')).catch(() => console.log('rejected'));
} const xxx = async() => {
runPromises()
await Promise.resolve()
console.log('last')
}
xxx() 3 const yyy = () => Promise.resolve(runPromises()).then(() => console.log('last'))
yyy() 3 |
Interesting. The behavior is only in console panels, which include Chrome, Firefox and Safari. |
@philipyoungg I understand it that way: I must say I forgot about that each |
What do people think about this approach? export class MyComponent extends React.Component {
constructor (props) {
super(props)
this.hasFinishedAsync = new Promise((resolve, reject) => {
this.finishedAsyncResolve = resolve
})
}
componentDidMount () {
this.doSomethingAsync()
}
async doSomethingAsync () {
try {
actuallyDoAsync()
this.props.callback()
this.finishedAsyncResolve('success')
} catch (error) {
this.props.callback()
this.finishedAsyncResolve('error')
}
}
// the rest of the component
} And in the tests: it(`should properly await for async code to finish`, () => {
const mockCallback = jest.fn()
const wrapper = shallow(<MyComponent callback={mockCallback}/>)
expect(mockCallback.mock.calls.length).toBe(0)
await wrapper.instance().hasFinishedAsync
expect(mockCallback.mock.calls.length).toBe(1)
}) I had an issue when the async call was not done straight in componentDidMount, but it was calling an async function, that was calling another async function and so on. If I added an extra async step in all the async chain, I'd need to add an extra I wonder if there's a reason why I shouldn't be using this approach or if this looks good to people. |
@dgrcode it seems like it works, but it adds code to your production implementation solely for testing, which imo is always a bad practice. |
Another solution:
|
Another solution is to wrap the check into
By setting the timeout we postpone our checks to the moment when all other promises (within |
That’s not actually ensuring that though; it’s a race condition. |
It may look like a race condition. Given the fact that no real network calls involved here, I assumed that tasks from the My assumptions could be wrong. UPD: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop |
@prawn-cake while it's certainly possible for |
If I set some props which will then cause state to be updated (via didComponentUpdate), how can I wait for the state to be updated before testing the UI? The following doesn't work wrapper.setProps({ downtimes: [downTimeMock] }, () => {
console.log(wrapper.debug())
}) However, if I set the state directly then the callback works, but want to avoid this. |
There’s not a good mechanism because react doesn’t expose one, I’m afraid. |
I just came across this and am interested in the community's opinion on: class App extends Component {
state = { ... }
async componentDidMount() {
this.setState({ loading: true })
const foo = await bar()
this.setState({ loading: false, ...foo })
}
render() {
... // render spinner if loading, else other stuff or whatever
}
}
it('should work', () => {
const spy = jest.spyOn(whateverModule, 'bar')
const app = new App(props)
app.setState = jest.fn()
app.componentDidMount.then(() => expect(spy).toHaveBeenCalled())
}); I'm guessing it's bad taste to In our case, we were testing that the component was starting a spinner, loading some data, and stopping the spinner. This test locks that functionality to the |
@mlodato517 both using new and spying on setState are practices i would strenuously avoid. You should use |
Yeah the spy on That makes sense then, didn't know you could do that! I saw the EDIT: async componentDidMount() {
this.setState({ loading: true })
const foo = await bar()
if (foo) { doOtherStuff() }
this.setState({ loading: false })
}
} so I don't think we can await any state fields? How does await state fields work ...? EDIT 2: EDIT 3: |
This topic has a huge amount of valuable information. Is there any newer approach for this? |
I hit the same issue. None of above ways work for my case. I am using the import React, { Component } from 'react';
import svc from './contants/svc';
class MFASection extends Component<any, any> {
constructor(props) {
super(props);
this.state = {
enabledMFA: true
};
}
componentDidMount() {
svc.getMe().then(res => {
console.log(res);
this.setState({ enabledMFA: res.data.mfa_enabled });
});
}
render() {
return <div>enabledMFA: {this.state.enabledMFA}</div>;
}
}
export default MFASection; import React from 'react';
import { shallow } from 'enzyme';
import MFASection from '.';
import svc from './contants/svc';
function flushPromises() {
return new Promise(resolve => setImmediate(resolve));
}
describe('MFASection', () => {
test('molecules/MFASection mounts', async () => {
const getMeSpy = jest.spyOn(svc, 'getMe').mockResolvedValueOnce({ data: { mfa_enabled: false } });
const wrapper = shallow(<MFASection></MFASection>);
expect(wrapper.exists()).toBe(true);
expect(wrapper.state('enabledMFA')).toBeTruthy();
await flushPromises();
expect(wrapper.text()).toBe('enabledMFA: false');
expect(getMeSpy).toBeCalledTimes(1);
});
}); Unit test result FAIL src/stackoverflow/58648463-todo/index.spec.tsx
MFASection
✕ molecules/MFASection mounts (30ms)
● MFASection › molecules/MFASection mounts
expect(received).toBe(expected) // Object.is equality
Expected: "enabledMFA: true"
Received: "enabledMFA: "
14 | expect(wrapper.state('enabledMFA')).toBeTruthy();
15 | await flushPromises();
> 16 | expect(wrapper.text()).toBe('enabledMFA: false');
| ^
17 | expect(getMeSpy).toBeCalledTimes(1);
18 | });
19 | });
at src/stackoverflow/58648463-todo/index.spec.tsx:16:28
at step (src/stackoverflow/58648463-todo/index.spec.tsx:33:23)
at Object.next (src/stackoverflow/58648463-todo/index.spec.tsx:14:53)
at fulfilled (src/stackoverflow/58648463-todo/index.spec.tsx:5:58)
console.log src/stackoverflow/58648463-todo/index.tsx:13
{ data: { mfa_enabled: false } }
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 4.207s, estimated 12s I expected the |
@mrdulin in your case, instead of "flushing promises", you should spy on |
@ljharb Do you mean like this? Solution 1: import React from 'react';
import { shallow } from 'enzyme';
import MFASection from '.';
import svc from './contants/svc';
describe('MFASection', () => {
test('molecules/MFASection mounts', done => {
const mRepsonse = { data: { mfa_enabled: false } };
let successHandler;
const getMeSpy = jest.spyOn(svc, 'getMe').mockImplementation((): any => {
const mThen = jest.fn().mockImplementationOnce((onfulfilled: any): any => {
successHandler = onfulfilled;
});
return { then: mThen };
});
const wrapper = shallow(<MFASection></MFASection>);
expect(wrapper.exists()).toBe(true);
expect(wrapper.state('enabledMFA')).toBeTruthy();
successHandler(mRepsonse);
expect(wrapper.text()).toBe('enabledMFA: 2');
expect(getMeSpy).toBeCalledTimes(1);
done();
});
});
Unit test resultFAIL src/stackoverflow/58648463-todo/index.spec.tsx (11.897s)
MFASection
✕ molecules/MFASection mounts (35ms)
● MFASection › molecules/MFASection mounts
expect(received).toBe(expected) // Object.is equality
Expected: "enabledMFA: false"
Received: "enabledMFA: "
23 | successHandler(mRepsonse);
24 | wrapper.update();
> 25 | expect(wrapper.text()).toBe('enabledMFA: false');
| ^
26 | expect(getMeSpy).toBeCalledTimes(1);
27 | done();
28 | });
at Object.<anonymous> (src/stackoverflow/58648463-todo/index.spec.tsx:25:28)
console.log src/stackoverflow/58648463-todo/index.tsx:13
{ data: { mfa_enabled: false } }
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 13.936s, estimated 14s UPDATE Sorry, it works. My fault. I forget that react will not render a primitive boolean value. After I change the JSX to
to
the test passes. Solution 2: use test('molecules/MFASection mounts - 2', done => {
const mRepsonse = { data: { mfa_enabled: false } };
const getMeSpy = jest.spyOn(svc, 'getMe').mockResolvedValueOnce(mRepsonse);
const wrapper = shallow(<MFASection></MFASection>);
expect(wrapper.exists()).toBe(true);
expect(wrapper.state('enabledMFA')).toBeTruthy();
setImmediate(() => {
expect(wrapper.text()).toBe('enabledMFA: 2');
done();
});
expect(getMeSpy).toBeCalledTimes(1);
}); Unit test result: PASS src/stackoverflow/58648463-todo/index.spec.tsx (9.834s)
MFASection
✓ molecules/MFASection mounts (19ms)
✓ molecules/MFASection mounts - 2 (4ms)
console.log src/stackoverflow/58648463-todo/index.tsx:13
{ data: { mfa_enabled: false } }
console.log src/stackoverflow/58648463-todo/index.tsx:13
{ data: { mfa_enabled: false } }
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 11.154s Source code of the example: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/58648463 |
Current behavior
When I have to wait for some data in
componentDidMount
, I can't test that after data is fetched component rendered with expected content or when async method failed, component rendered error.I used create-react-app in version 1.5.2.
Enzyme in version 3.3.0.
My App.js:
App.test.js:
Result:
I've tried several methods mentioned here: #346 .
Test for positive case works, but for negative doesn't, so I think that there must by an issue with entire approach, and "green light" for first test is just an accident.
Expected behavior
I would like to have way to test both positive and negative paths.
API
Version
Adapter
The text was updated successfully, but these errors were encountered: