【问题标题】:Jest: Wait for called .then of async method in componentDidMount笑话:等待 componentDidMount 中异步方法的调用 .then
【发布时间】:2019-12-06 03:21:24
【问题描述】:

我目前正忙于编写我的 React-App 测试。

我的 componentDidMount 方法中有一个异步调用,并在返回后更新状态。但是,我没有得到这个工作。

我找到了几种解决方案,但似乎都没有按预期工作。下面是我到过的最近的点。

应用:

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 将被调用两次。

这是我尝试过但没有成功的想法:

  • 监视 thing.initialize() -> 在方法被调用并完成之后,我没有发现我是如何进行测试的。
  • 监视 App.test -> 此处相同
  • 使用承诺而不是异步等待
  • 一开始,我的componentDidMount中有一个thing.initialize().then(this.test)

不会太多,但有人能告诉我我缺少哪一部分吗?

【问题讨论】:

  • 您真的需要保留thing.initialize() 才能成为真正的电话吗?对我来说,测试的代码看起来不错(实际上你不需要wrapper.update()),但它只有在thing.initialize() 立即解决/拒绝时才会起作用。如果这是一个非零延迟的真正调用,那么您的测试在响应到来之前就完成了。
  • 是的,在调用 app.test 之前进行测试,这正是问题所在。 Thing.initialize 是一个异步函数,它获取一些数据并将其存储在本地存储中。因此它不会(我认为不能)立即返回。
  • 是单元测试还是集成测试?
  • 我想测试初始化​​后呈现的应用页面。作为替代方案,我可以创建一个在初始化后呈现的内容的组件,并在显式调用 thing.initialize 后对其进行测试。但我认为这将是一种应该以某种方式工作的解决方法。

标签: reactjs async-await jestjs


【解决方案1】:

如果这是集成测试,您最好遵循 等待 方法,即使用 Selenium:即等到某个元素出现或达到超时。它应该如何编码取决于您使用的库(对于 Puppeter,它应该是 waitForSelector)。

一旦涉及到单元测试,那么我建议您采用不同的方法:

  1. 使用您控制的Promise 模拟每个外部依赖项(通过您的代码很难说automatic mock 是否有效,或者您需要编写mock factory,但其中之一或两者都会有所帮助)
  2. 初始化元素(我的意思是运行shallow()mount()
  3. 等到你的模拟被解决(额外的await,使用setTimeout(... ,0)flush-promises 将起作用,检查how microtasks/macrotasks works
  4. 断言元素的render 并检查您的模拟是否已被调用

最后:

  1. 直接设置状态
  2. 模拟/监视内部方法
  3. 根据状态进行验证

都会导致不稳定的测试,因为它是您在单元测试期间不应该担心的实现细节。而且无论如何都很难与他们合作。

所以你的测试看起来像:

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);
})

【讨论】:

  • 谢谢!这对我有用。这不完全是我的想法,因为我不想模拟 thing.initialize。但是我可以单独测试它并在所有测试之前调用初始化一次,这将是安全的时间。
猜你喜欢
  • 2019-01-22
  • 1970-01-01
  • 2015-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多