开玩笑:等待在 componentDidMount 中调用异步方法的 .then
Jest: Wait for called .then of async method in componentDidMount
我目前正在编写我的 React-App 测试。
我在我的 componentDidMount 方法中有一个异步调用,并在返回后更新状态。但是,我没有让它工作。
我找到了几个解决方案,None 似乎按预期工作。下面是我到达的最近点。
应用程序:
class App extends Component<{}, IState> {
state: IState = {
fetchedData: false
};
async componentDidMount() {
await thing.initialize();
this.test();
}
test = () => {
this.setState({ fetchedData: true })
};
render() {
return this.state.fetchedData ? (
<div>Hello</div>
) : (
<Spinner />
);
}
}
测试
it('Base test for app', async () => {
const spy = spyOn(thing, 'initialize').and.callThrough(); // just for debugging
const wrapper = await mount(<App />);
// await wrapper.instance().componentDidMount(); // With this it works, but componentDidMount is called twice.
wrapper.update();
expect(wrapper.find('Spinner').length).toBe(0);
});
好吧,所以...thing.initialize 被调用(它是一个获取一些东西的异步方法)。
如果我明确调用 wrapper.instance().componentDidMount() 那么它会起作用,但是 componentDidMount 会被调用两次。
以下是我尝试过但 None 成功的想法:
- 监视 thing.initialize() -> 我没有找到如何进行测试 在 方法被调用并完成后。
- 监视 App.test -> 同上
- 使用 promises 而不是 async await
- 一开始,我的 componentDidMount
中有一个 thing.initialize().then(this.test)
应该不多,但谁能告诉我我少了哪一块?
如果这是集成测试你最好遵循等待方法说Selenium使用:也就是说,等到某个元素出现或超时。它应该如何编码取决于你使用的库(对于 Puppeter 它应该是 waitForSelector)。
一旦涉及到单元测试,那么我建议你采用不同的方法:
- 用您控制的
Promise
模拟每个外部依赖项(根据您的代码,很难说 automatic mock will work or you need to compose mock factory 是否有帮助,但其中一个或两者都有帮助)
- 初始化元素(我的意思是 运行
shallow()
或 mount()
)
- 等到你的模拟被解决(额外的
await
,使用 setTimeout(... ,0)
或 flush-promises
will work, check how microtasks/macrotasks works)
- 断言元素的
render
并检查您的模拟是否已被调用
最后:
- 直接设置状态
- mocking/spying 关于内部方法
- 验证状态
都会导致不稳定的测试,因为它是您在单元测试期间不应该担心的实现细节。而且无论如何都很难和他们一起工作。
所以你的测试看起来像:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('loads data and renders it', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.resolve(/*data you expect to return*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(1);
})
或者您可以测试组件在拒绝时的行为:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('renders error message on loading failuer', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.reject(/*some error data*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
expect(wrapper.find(SomeErrorMessage)).toHaveLength(1);
})
我目前正在编写我的 React-App 测试。
我在我的 componentDidMount 方法中有一个异步调用,并在返回后更新状态。但是,我没有让它工作。
我找到了几个解决方案,None 似乎按预期工作。下面是我到达的最近点。
应用程序:
class App extends Component<{}, IState> {
state: IState = {
fetchedData: false
};
async componentDidMount() {
await thing.initialize();
this.test();
}
test = () => {
this.setState({ fetchedData: true })
};
render() {
return this.state.fetchedData ? (
<div>Hello</div>
) : (
<Spinner />
);
}
}
测试
it('Base test for app', async () => {
const spy = spyOn(thing, 'initialize').and.callThrough(); // just for debugging
const wrapper = await mount(<App />);
// await wrapper.instance().componentDidMount(); // With this it works, but componentDidMount is called twice.
wrapper.update();
expect(wrapper.find('Spinner').length).toBe(0);
});
好吧,所以...thing.initialize 被调用(它是一个获取一些东西的异步方法)。 如果我明确调用 wrapper.instance().componentDidMount() 那么它会起作用,但是 componentDidMount 会被调用两次。
以下是我尝试过但 None 成功的想法:
- 监视 thing.initialize() -> 我没有找到如何进行测试 在 方法被调用并完成后。
- 监视 App.test -> 同上
- 使用 promises 而不是 async await
- 一开始,我的 componentDidMount 中有一个 thing.initialize().then(this.test)
应该不多,但谁能告诉我我少了哪一块?
如果这是集成测试你最好遵循等待方法说Selenium使用:也就是说,等到某个元素出现或超时。它应该如何编码取决于你使用的库(对于 Puppeter 它应该是 waitForSelector)。
一旦涉及到单元测试,那么我建议你采用不同的方法:
- 用您控制的
Promise
模拟每个外部依赖项(根据您的代码,很难说 automatic mock will work or you need to compose mock factory 是否有帮助,但其中一个或两者都有帮助) - 初始化元素(我的意思是 运行
shallow()
或mount()
) - 等到你的模拟被解决(额外的
await
,使用setTimeout(... ,0)
或flush-promises
will work, check how microtasks/macrotasks works) - 断言元素的
render
并检查您的模拟是否已被调用
最后:
- 直接设置状态
- mocking/spying 关于内部方法
- 验证状态
都会导致不稳定的测试,因为它是您在单元测试期间不应该担心的实现细节。而且无论如何都很难和他们一起工作。
所以你的测试看起来像:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('loads data and renders it', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.resolve(/*data you expect to return*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(1);
})
或者您可以测试组件在拒绝时的行为:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('renders error message on loading failuer', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.reject(/*some error data*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
expect(wrapper.find(SomeErrorMessage)).toHaveLength(1);
})