使用 Enzyme 进行 React 单元测试不会重新绑定辅助函数的上下文
React unit tests with Enzyme don't re-bind context of helper functions
这是我在尝试使用 AirBnB 的 React 测试库重构我的一些 React 组件时遇到的一个有趣的问题,Enzyme。
我认为解释我的问题的最好方法是通过示例。
这是一个小型 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!');
});
});
这工作正常,但测试组件的当前实现效率不高。通过使用 .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)
,它确认构造函数仍在我预期的时候被调用,并且它接收的 props 是正确的。但是,我在 renderInnerSpan
中添加了一个 console.log(foo)
,看起来该值始终为真,即使在重新渲染组件并将其 foo
属性显式设置为 false
.
看起来 renderInnerSpan
只绑定了一次,Enzyme 正在为每个测试重新使用它。那么,是什么给了?我在测试中重新创建我的组件,它使用我期望的值调用它的构造函数 - 为什么我的绑定 renderInnerSpan
函数继续使用旧值?
在此先感谢您的帮助。
在我看来,您在 class 之外定义了 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!');
});
这里的问题是一个函数不能绑定多次,正如您在测试用例中尝试的那样。
原因是上下文不仅仅是函数本身的 属性。当一个函数被绑定时,它被包裹在 bound function exotic object.
中
上下文(this
-赋值)保存在外来对象的[[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 组件转换成一个 stateless function:
import renderInnerSpan from './renderInnerSpan'
export default (props) => (
<div>
{renderInnerSpan(props.foo)}
</div>
);
更好看、更易读! :-)
这是我在尝试使用 AirBnB 的 React 测试库重构我的一些 React 组件时遇到的一个有趣的问题,Enzyme。
我认为解释我的问题的最好方法是通过示例。
这是一个小型 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!');
});
});
这工作正常,但测试组件的当前实现效率不高。通过使用 .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)
,它确认构造函数仍在我预期的时候被调用,并且它接收的 props 是正确的。但是,我在 renderInnerSpan
中添加了一个 console.log(foo)
,看起来该值始终为真,即使在重新渲染组件并将其 foo
属性显式设置为 false
.
看起来 renderInnerSpan
只绑定了一次,Enzyme 正在为每个测试重新使用它。那么,是什么给了?我在测试中重新创建我的组件,它使用我期望的值调用它的构造函数 - 为什么我的绑定 renderInnerSpan
函数继续使用旧值?
在此先感谢您的帮助。
在我看来,您在 class 之外定义了 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!');
});
这里的问题是一个函数不能绑定多次,正如您在测试用例中尝试的那样。
原因是上下文不仅仅是函数本身的 属性。当一个函数被绑定时,它被包裹在 bound function exotic object.
中上下文(this
-赋值)保存在外来对象的[[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 组件转换成一个 stateless function:
import renderInnerSpan from './renderInnerSpan'
export default (props) => (
<div>
{renderInnerSpan(props.foo)}
</div>
);
更好看、更易读! :-)