如何对 fetch 完成后呈现的 React 组件进行单元测试?

Posted

技术标签:

【中文标题】如何对 fetch 完成后呈现的 React 组件进行单元测试?【英文标题】:How to unit test a React component that renders after fetch has finished? 【发布时间】:2018-01-08 16:42:31 【问题描述】:

我是 Jest/React 初学者。在开玩笑的it 中,我需要等到所有承诺都执行完毕后再进行实际检查。

我的代码是这样的:

export class MyComponent extends Component 
    constructor(props) 
        super(props);
        this.state =  /* Some state */ ;
    

    componentDidMount() 
        fetch(some_url)
            .then(response => response.json())
            .then(json => this.setState(some_state);
    

    render() 
        // Do some rendering based on the state
    

当组件被挂载时,render() 运行两次:一次在构造函数运行之后,一次在fetch()(在componentDidMount())完成并且链式 Promise 完成执行之后。

我的测试代码是这样的:

describe('MyComponent', () => 

    fetchMock.get('*', some_response);

    it('renders something', () => 
        let wrapper = mount(<MyComponent />);
        expect(wrapper.find(...)).to.have.something();
    ;

无论我从it 返回什么,它都会在render() 第一次执行之后但第二次之前运行。例如,如果我返回 fetchMock.flush().then(() =&gt; expect(...)),则返回的 Promise 在第二次调用 render() 之前执行(我相信我能理解为什么)。

如何等到第二次调用 render() 后再运行 expect()

【问题讨论】:

在我看来,您试图在一次测试中测试太多东西。您要测试的是,您的 fetch 函数在组件挂载时被调用,然后您有多个其他测试将状态显式传递给它们,您可以检查组件是否正确呈现。 @MattWatson 如果我检查 (1) 是否调用了 fetch 函数并且 (2) 是否正确传递了状态,那么我无法检查 (1.5) 状态是否设置正确。如何检查状态是否设置正确? 【参考方案1】:

我将关注点分开,主要是因为更易于维护和测试。我不会在组件内部声明 fetch,而是在其他地方进行,例如在 redux 操作中(如果使用 redux)。

然后单独测试 fetch 和组件,毕竟这是单元测试。

对于异步测试,您可以在测试中使用done 参数。例如:

describe('Some tests', () => 
  fetchMock.get('*', some_response);

  it('should fetch data', (done) =>  // <---- Param
    fetchSomething( some: 'Params' )
      .then(result => 
        expect(result).toBe( whatever: 'here' );
        done(); // <--- When you are done
      );
  );
)

你可以通过在 props 中发送加载的数据来测试你的组件。

describe('MyComponent', () => 

  it('renders something', () => 
    const mockResponse =  some: 'data' ;
    let wrapper = mount(<MyComponent data=mockResponse/>);

    expect(wrapper.find(...)).to.have.something();
  );
);

当涉及到测试时,您需要保持简单,如果您的组件难以测试,那么您的设计就有问题;)

【讨论】:

有没有可能按照我的原意去做?我问是为了更好地掌握异步编程。 Jest 用于单元测试,您尝试做的不是单元测试。话虽如此,您可以从测试中手动设置状态,例如wrapper.setState( some_state ) 然后expect(wrapper.find(...)).to.have.something();。要测试 fetch... 除了将其从组件中取出之外,这里没有太多选择;)【参考方案2】:

我找到了一种方法来做我最初要求的事情。我没有意见(还)它是否是好的策略(事实上我必须在之后立即重构组件,所以这个问题与我正在做的事情不再相关)。无论如何,这是测试代码(解释如下):

import React from 'react';
import  mount  from 'enzyme';
import  MyComponent  from 'wherever';
import fetchMock from 'fetch-mock';

let _resolveHoldingPromise = false;

class WrappedMyComponent extends MyComponent 

    render() 
        const result = super.render();
        _resolveHoldingPromise && _resolveHoldingPromise();
        _resolveHoldingPromise = false;
        return result;
    

    static waitUntilRender() 
        // Create a promise that can be manually resolved
        let _holdingPromise = new Promise(resolve =>
            _resolveHoldingPromise = resolve);

        // Return a promise that will resolve when the component renders
        return Promise.all([_holdingPromise]);
    


describe('MyComponent', () => 

    fetchMock.get('*', 'some_response');

    const onError = () =>  throw 'Internal test error'; ;

    it('renders MyComponent appropriately', done => 
        let component = <WrappedMyComponent />;
        let wrapper = mount(component);
        WrappedMyComponent.waitUntilRender().then(
            () => 
                expect(wrapper.find('whatever')).toBe('whatever');
                done();
            ,
            onError);
    );
);

主要思想是,在测试代码中,我将组件子类化(如果这是 Python,我可能会对其进行猴子补丁,在这种情况下或多或少地工作方式相同),以便它的 render()方法发送一个它执行的信号。发送信号的方式是手动解决一个promise。当一个promise被创建时,它会创建两个函数,resolve和reject,当被调用时会终止promise。让 Promise 之外的代码解析 Promise 的方法是让 Promise 将对其解析函数的引用存储在外部变量中。

感谢 fetch-mock 作者 Rhys Evans 向我解释了手动解决承诺的技巧。

【讨论】:

那是个坏消息。我希望这不是唯一的方法!【参考方案3】:

我在这方面取得了一些成功,因为它不需要包装或修改组件。然而,假设组件中只有一个fetch(),但如果需要,可以轻松修改。

// testhelper.js

class testhelper

    static async waitUntil(fnWait) 
        return new Promise((resolve, reject) => 
            let count = 0;
            function check() 
                if (++count > 20) 
                    reject(new TypeError('Timeout waiting for fetch call to begin'));
                    return;
                
                if (fnWait()) resolve();
                setTimeout(check, 10);
            
            check();
        );
    

    static async waitForFetch(fetchMock)
    
        // Wait until at least one fetch() call has started.
        await this.waitUntil(() => fetchMock.called());

        // Wait until active fetch calls have completed.
        await fetchMock.flush();
    


export default testhelper;

然后你可以在你的断言之前使用它:

import testhelper from './testhelper.js';

it('example', async () => 
    const wrapper = mount(<MyComponent/>);

    // Wait until all fetch() calls have completed
    await testhelper.waitForFetch(fetchMock);

    expect(wrapper.html()).toMatchSnapshot();
);

【讨论】:

太丑了……这仍然是最先进的吗? 不确定,因为我最近在这方面做得不多,但你不喜欢怎么办? 倒计时和计时器。如果有一些processAllPromises 或其他东西会很好。我最终在 react 测试库中使用了findBy(我正在测试 React):testing-library.com/docs/dom-testing-library/api-queries/… 它仍然有一个计时器/超时,它可以像这样实现!但至少我不必写或看到它:-D 您不必使用倒计时/计时器,它只是作为备份,以避免在承诺永远不会完成时 CI/CD 服务器挂起。您可以搜索“flush promises”,详细了解如何确保处理所有承诺。

以上是关于如何对 fetch 完成后呈现的 React 组件进行单元测试?的主要内容,如果未能解决你的问题,请参考以下文章

在 fetch 和 mount 完成后做一些事情

无法使用 fetch POST 方法对未安装的组件执行 React 状态更新

fetch() 完成后如何调用函数

React Update Fetch on Checkbox Click

动画完成后 React-Native ActivityIndi​​cator 不隐藏

如何从一个组件导入功能对另一个组件做出反应以呈现它?