首页 文章

使用酶反应单元测试不会重新绑定辅助函数的上下文

提问于
浏览
2

这是一个有趣的问题,我在尝试使用AirBnB的React测试库Enzyme重构我的一些React组件时遇到了这个问题 .

我认为解释我的问题的最好方法是通过一个例子 .

这是一个小的React组件,它将显示一条消息,具体取决于它从其父组件接收的props:

test.js:

import React from 'react';

function renderInnerSpan() {
    const {foo} = this.props;

    if (foo) {
        return <span>Foo is truthy!</span>;
    }

    return <span>Foo is falsy!</span>;
}

export default class extends React.Component {
    render() {
        return (
            <div>
                {renderInnerSpan.call(this)}
            </div>
        );
    }    
}

这是这个组件的测试套件,有两个通过的测试:

test.spec.js:

import Test from '../../src/test';

import React from 'react';
import {shallow} from 'enzyme';
import {expect} from 'chai';

describe('Test Suite', () => {
    let renderedElement,
        expectedProps;

    function renderComponent() {
        const componentElement = React.createElement(Test, expectedProps);

        renderedElement = shallow(componentElement);
    }

    beforeEach(() => {
        expectedProps = {
            foo: true
        };

        renderComponent();
    });

    it('should display the correct message for truthy values', () => {
        const span = renderedElement.props().children;

        expect(span.props.children).to.equal('Foo is truthy!');
    });

    it('should display the correct message for falsy values', () => {
        expectedProps.foo = false;
        renderComponent();

        const span = renderedElement.props().children;

        expect(span.props.children).to.equal('Foo is falsy!');
    });
});

这工作正常,但测试组件's current implementation isn' t尽可能高效 . 通过使用 .call(this) ,每次调用 render() 函数时它都会创建一个新函数 . 我可以通过在组件的构造函数中绑定 this 的正确上下文来避免这种情况,如下所示:

export default class extends React.Component {
    constructor(props) {
        super(props);

        renderInnerSpan = renderInnerSpan.bind(this);
    }

    render() {
        return (
            <div>
                {renderInnerSpan()}
            </div>
        );
    }
}

在此更改之后,组件仍按预期工作,但测试开始失败:

AssertionError: expected 'Foo is truthy!' to equal 'Foo is falsy!'
Expected :Foo is falsy!
Actual   :Foo is truthy!

我在构造函数中添加了一个 console.log(props.foo) ,它确认构造函数在我预期的时候仍然被调用,并且它接收的道具是正确的 . 但是,我在 renderInnerSpan 中添加了一个 console.log(foo) ,看起来该值始终为true,即使在将其 foo prop显式设置为 false 重新渲染组件之后也是如此 .

看起来 renderInnerSpan 只被绑定一次,而Enzyme正在重复使用它进行每次测试 . 那么,是什么给出的?我在测试中重新创建我的组件,它使用我期望的值调用它的构造函数 - 为什么我的绑定 renderInnerSpan 函数继续使用旧值?

在此先感谢您的帮助 .

2 回答

  • -1

    这里的问题是 a function cannot be bound multiple times ,正如您在测试用例中所尝试的那样 .

    原因是上下文不仅仅是函数本身的属性 . 当一个函数被绑定时,它被包装在bound function exotic object中 .

    上下文( this -assignment)保存在外来对象的 [[BoundThis]] 属性中 . 绑定函数将始终使用此上下文调用,即使它再次绑定也是如此 .


    你可以自己测试一下:

    function foo() {
      console.log(this.bar);
    }
    
    foo(); // undefined
    
    foo = foo.bind({bar: 1});
    foo(); // 1
    
    foo = foo.bind({bar: 2});
    foo(); // 1
    

    To solve this issue, I suggest you remove the dependency to the context from the rendering function and transfer all required input via function parameters instead:

    function renderInnerSpan(foo) {
        if (foo) {
            return <span>Foo is truthy!</span>;
        }
    
        return <span>Foo is falsy!</span>;
    }
    
    export default class extends React.Component {   
        render() {
            return (
                <div>
                    {renderInnerSpan(this.props.foo)}
                </div>
            );
        }
    }
    

    这将删除隐藏的依赖项,并使代码更具可读性和可维护性 . 如果您决定将渲染功能移动到自己的模块,您现在可以轻松地执行此操作 .

    由于您不再需要在构造函数中绑定函数上下文,您甚至可以将React组件转换为stateless function

    import renderInnerSpan from './renderInnerSpan'
    
    export default (props) => (
        <div>
            {renderInnerSpan(props.foo)}
        </div>
    );
    

    更好,更可读! :-)

  • 1

    在我看来,你在类之外定义了renderInnerSpan函数,这可能会产生一些问题 .

    试试这个:

    import React from 'react';
    
    
    export default class extends React.Component {
      render() {
        return (
          <div>
            {this.renderInnerSpan.bind(this)}
          </div>
        );
      }
    
      renderInnerSpan() {
        const {foo} = this.props;
    
        if (foo) {
          return <span>Foo is truthy!</span>;
        }
    
        return <span>Foo is falsy!</span>;
      }
    }
    

    另一件事是你的renderComponent函数可以像这样重写:

    function renderComponent(expectedProps) {
        const componentElement = React.createElement(Test, expectedProps);
    
        return shallow(componentElement);
    }
    

    如果你在每个测试中更改道具,那么就没有理由在beforeEach块中设置道具 . 只需在每个测试中使用新的renderComponent .

    it('should display the correct message for truthy values', () => {
        renderedElement = renderComponent({foo: true});
        const span = renderedElement.props().children;
    
        expect(span.props.children).to.equal('Foo is truthy!');
    });
    

相关问题