2

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

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

这是一个小的 React 组件,它将根据从其父组件接收到的 props 显示一条消息:

测试.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!');
    });
});

这可以正常工作,但 Test 组件的当前实现并没有达到应有的效率。通过使用.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)在构造函数中添加了一个,它确认构造函数在我期望的时候仍然被调用,并且它接收的道具是正确的。但是,我添加了一个console.log(foo)inside renderInnerSpan,并且看起来该值始终为 true,即使在重新渲染组件并将其fooprop 明确设置为false.

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

在此先感谢您的帮助。

4

2 回答 2

1

这里的问题是一个函数不能被多次绑定,就像您在测试用例中尝试的那样。

原因是上下文不仅仅是函数本身的属性。当一个函数被绑定时,它被包裹在一个绑定的函数外来对象中。

上下文(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


为了解决这个问题,我建议您从渲染函数中删除对上下文的依赖,并通过函数参数传输所有必需的输入:

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 组件转换为无状态函数

import renderInnerSpan from './renderInnerSpan'

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

更好,更具可读性!:-)

于 2016-07-19T13:39:37.293 回答
-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!');
});
于 2016-07-19T05:26:22.643 回答