如何使用浅层渲染测试装饰过的 React 组件
How to test decorated React component with shallow rendering
我正在学习本教程:http://reactkungfu.com/2015/07/approaches-to-testing-react-components-an-overview/
正在尝试了解 "shallow rendering" 的工作原理。
我有一个高阶组件:
import React from 'react';
function withMUI(ComposedComponent) {
return class withMUI {
render() {
return <ComposedComponent {...this.props}/>;
}
};
}
和一个组件:
@withMUI
class PlayerProfile extends React.Component {
render() {
const { name, avatar } = this.props;
return (
<div className="player-profile">
<div className='profile-name'>{name}</div>
<div>
<Avatar src={avatar}/>
</div>
</div>
);
}
}
和一个测试:
describe('PlayerProfile component - testing with shallow rendering', () => {
beforeEach(function() {
let {TestUtils} = React.addons;
this.TestUtils = TestUtils;
this.renderer = TestUtils.createRenderer();
this.renderer.render(<PlayerProfile name='user'
avatar='avatar'/>);
});
it('renders an Avatar', function() {
let result = this.renderer.getRenderOutput();
console.log(result);
expect(result.type).to.equal(PlayerProfile);
});
});
result
变量持有 this.renderer.getRenderOutput()
在教程中,result.type
的测试方式如下:
expect(result.type).toEqual('div');
在我的例子中,如果我记录 result
它是:
LOG: Object{type: function PlayerProfile() {..}, .. }
所以我改变了我的测试:
expect(result.type).toEqual(PlayerProfile)
现在它给我这个错误:
Assertion Error: expected [Function: PlayerProfile] to equal [Function: withMUI]
所以PlayerProfile
的类型是高阶函数withMUI
.
PlayerProfile
修饰withMUI
,使用浅层渲染,只有PlayerProfile
组件被渲染,而不是children。所以浅渲染不适用于我假设的装饰组件。
我的问题是:
为什么在教程中 result.type
应该是 div,但在我的例子中不是。
如何使用浅层渲染测试装饰有高阶组件的 React 组件?
你不能。首先让我们稍微去除装饰器的糖分:
let PlayerProfile = withMUI(
class PlayerProfile extends React.Component {
// ...
}
);
withMUI returns 一个不同的 class,所以 PlayerProfile class 只存在于 withMUI 的闭包中。
这是一个简化版本:
var withMUI = function(arg){ return null };
var PlayerProfile = withMUI({functionIWantToTest: ...});
你将值传递给函数,它没有返回,你没有值。
解决办法?保留对它的引用。
// no decorator here
class PlayerProfile extends React.Component {
// ...
}
然后我们可以导出组件的包装和未包装版本:
// this must be after the class is declared, unfortunately
export default withMUI(PlayerProfile);
export let undecorated = PlayerProfile;
使用这个组件的正常代码不会改变,但您的测试将使用这个:
import {undecorated as PlayerProfile} from '../src/PlayerProfile';
另一种方法是将 withMUI 函数模拟为 (x) => x
(标识函数)。这可能会导致奇怪的副作用,需要从测试端完成,因此您的测试和源代码可能会在添加装饰器时不同步。
这里不使用装饰器似乎是安全的选择。
我认为上面的示例令人困惑,因为 decorator
概念与 "higher order component" 的概念可互换使用。我通常将它们组合使用,这将使 testing/rewire/mocking 更容易。
我会使用装饰器来:
- 向子组件提供 props,通常是 bind/listen 到 flux store
在哪里我会使用更高阶的组件
- 以更具声明性的方式绑定上下文
重新布线的问题是我认为您不能重新布线在导出 function/class 之外应用的任何内容,装饰器就是这种情况。
如果您想使用装饰器和高阶组件的组合,您可以执行以下操作:
//withMui-decorator.jsx
function withMUI(ComposedComponent) {
return class withMUI extends Component {
constructor(props) {
super(props);
this.state = {
store1: ///bind here based on some getter
};
}
render() {
return <ComposedComponent {...this.props} {...this.state} {...this.context} />;
}
};
}
//higher-order.jsx
export default function(ChildComp) {
@withMui //provide store bindings
return class HOC extends Component {
static childContextTypes = {
getAvatar: PropTypes.func
};
getChildContext() {
let {store1} = this.props;
return {
getAvatar: (id) => ({ avatar: store1[id] });
};
}
}
}
//child.js
export default Child extends Component {
static contextTypes = {
getAvatar: PropTypes.func.isRequired
};
handleClick(id, e) {
let {getAvatar} = this.context;
getAvatar(`user_${id}`);
}
render() {
let buttons = [1,2,3].map((id) => {
return <button type="text" onClick={this.handleClick.bind(this, id)}>Click Me</button>
});
return <div>{buttons}</div>;
}
}
//index.jsx
import HOC from './higher-order';
import Child from './child';
let MyComponent = HOC(Child);
React.render(<MyComponent {...anyProps} />, document.body);
然后当你想测试时,你可以很容易地"rewire"你的商店由装饰器提供,因为装饰器在导出的高阶组件内;
//spec.js
import HOC from 'higher-order-component';
import Child from 'child';
describe('rewire the state', () => {
let mockedMuiDecorator = function withMUI(ComposedComponent) {
return class withMUI extends Component {
constructor(props) {
super(props);
this.state = {
store1: ///mock that state here to be passed as props
};
}
render() {
//....
}
}
}
HOC.__Rewire__('withMui', mockedMuiDecorator);
let MyComponent = HOC(Child);
let child = TestUtils.renderIntoDocument(
<MyComponent {...mockedProps} />
);
let childElem = React.findDOMNode(child);
let buttons = childElem.querySelectorAll('button');
it('Should render 3 buttons', () => {
expect(buttons.length).to.equal(3);
});
});
我很确定这并没有真正回答您最初的问题,但我认为您在协调何时使用装饰器时遇到问题 vs.higher 订购组件。
这里有一些很好的资源:
- http://jaysoo.ca/2015/06/09/react-contexts-and-dependency-injection/
- https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750
- https://github.com/badsyntax/react-seed/blob/master/app/components/Menu/tests/Menu-test.jsx
- https://github.com/Yomguithereal/baobab-react/blob/master/test/suites/higher-order.jsx
您可以使用 'babel-plugin-remove-decorators' 插件。此解决方案将让您正常编写组件,而无需导出装饰和未装饰的组件。
先安装插件,然后创建一个包含以下内容的文件,我们称之为'babelTestingHook.js'
require('babel/register')({
'stage': 2,
'optional': [
'es7.classProperties',
'es7.decorators',
// or Whatever configs you have
.....
],
'plugins': ['babel-plugin-remove-decorators:before']
});
和运行你的下面的测试将忽略装饰器,你将能够正常测试组件
mocha ./tests/**/*.spec.js --require ./babelTestingHook.js --recursive
在我的例子中,装饰器非常有用,我不想在我的应用程序中摆脱它们(或 return 包装和未包装的版本)。
我认为最好的方法是使用 Qusai 说的 babel-plugin-remove-decorators
(可用于在测试中删除它们),但我编写的预处理器不同,如下所示:
'use strict';
var babel = require('babel-core');
module.exports = {
process: function(src, filename) {
// Ignore files other than .js, .es, .jsx or .es6
if (!babel.canCompile(filename)) {
return '';
}
if (filename.indexOf('node_modules') === -1) {
return babel.transform(src, {
filename: filename,
plugins: ['babel-plugin-remove-decorators:before']
}).code;
}
return src;
}
};
注意 babel.transform
调用,我将 babel-plugin-remove-decorators:before
元素作为数组值传递,请参阅:https://babeljs.io/docs/usage/options/
要将其与 Jest(我使用的是它)连接起来,您可以在 package.json
:
中使用如下设置来完成
"jest": {
"rootDir": "./src",
"scriptPreprocessor": "../preprocessor.js",
"unmockedModulePathPatterns": [
"fbjs",
"react"
]
},
其中 preprocessor.js
是预处理器的名称。
使用 Enzyme 测试高阶/浅层装饰器
使用名为 dive()
的方法
关注此 link,了解潜水的工作原理
https://github.com/airbnb/enzyme/blob/master/docs/api/ShallowWrapper/dive.md
所以你可以浅化更高阶的组件,然后深入内部。
在上面的例子中:
const wrapper=shallow(<PlayerProfile name={name} avatar={}/>)
expect(wrapper.find("PlayerProfile").dive().find(".player-profile").length).toBe(1)
同样,您可以访问属性并对其进行测试。
我正在学习本教程:http://reactkungfu.com/2015/07/approaches-to-testing-react-components-an-overview/
正在尝试了解 "shallow rendering" 的工作原理。
我有一个高阶组件:
import React from 'react';
function withMUI(ComposedComponent) {
return class withMUI {
render() {
return <ComposedComponent {...this.props}/>;
}
};
}
和一个组件:
@withMUI
class PlayerProfile extends React.Component {
render() {
const { name, avatar } = this.props;
return (
<div className="player-profile">
<div className='profile-name'>{name}</div>
<div>
<Avatar src={avatar}/>
</div>
</div>
);
}
}
和一个测试:
describe('PlayerProfile component - testing with shallow rendering', () => {
beforeEach(function() {
let {TestUtils} = React.addons;
this.TestUtils = TestUtils;
this.renderer = TestUtils.createRenderer();
this.renderer.render(<PlayerProfile name='user'
avatar='avatar'/>);
});
it('renders an Avatar', function() {
let result = this.renderer.getRenderOutput();
console.log(result);
expect(result.type).to.equal(PlayerProfile);
});
});
result
变量持有 this.renderer.getRenderOutput()
在教程中,result.type
的测试方式如下:
expect(result.type).toEqual('div');
在我的例子中,如果我记录 result
它是:
LOG: Object{type: function PlayerProfile() {..}, .. }
所以我改变了我的测试:
expect(result.type).toEqual(PlayerProfile)
现在它给我这个错误:
Assertion Error: expected [Function: PlayerProfile] to equal [Function: withMUI]
所以PlayerProfile
的类型是高阶函数withMUI
.
PlayerProfile
修饰withMUI
,使用浅层渲染,只有PlayerProfile
组件被渲染,而不是children。所以浅渲染不适用于我假设的装饰组件。
我的问题是:
为什么在教程中 result.type
应该是 div,但在我的例子中不是。
如何使用浅层渲染测试装饰有高阶组件的 React 组件?
你不能。首先让我们稍微去除装饰器的糖分:
let PlayerProfile = withMUI(
class PlayerProfile extends React.Component {
// ...
}
);
withMUI returns 一个不同的 class,所以 PlayerProfile class 只存在于 withMUI 的闭包中。
这是一个简化版本:
var withMUI = function(arg){ return null };
var PlayerProfile = withMUI({functionIWantToTest: ...});
你将值传递给函数,它没有返回,你没有值。
解决办法?保留对它的引用。
// no decorator here
class PlayerProfile extends React.Component {
// ...
}
然后我们可以导出组件的包装和未包装版本:
// this must be after the class is declared, unfortunately
export default withMUI(PlayerProfile);
export let undecorated = PlayerProfile;
使用这个组件的正常代码不会改变,但您的测试将使用这个:
import {undecorated as PlayerProfile} from '../src/PlayerProfile';
另一种方法是将 withMUI 函数模拟为 (x) => x
(标识函数)。这可能会导致奇怪的副作用,需要从测试端完成,因此您的测试和源代码可能会在添加装饰器时不同步。
这里不使用装饰器似乎是安全的选择。
我认为上面的示例令人困惑,因为 decorator
概念与 "higher order component" 的概念可互换使用。我通常将它们组合使用,这将使 testing/rewire/mocking 更容易。
我会使用装饰器来:
- 向子组件提供 props,通常是 bind/listen 到 flux store
在哪里我会使用更高阶的组件
- 以更具声明性的方式绑定上下文
重新布线的问题是我认为您不能重新布线在导出 function/class 之外应用的任何内容,装饰器就是这种情况。
如果您想使用装饰器和高阶组件的组合,您可以执行以下操作:
//withMui-decorator.jsx
function withMUI(ComposedComponent) {
return class withMUI extends Component {
constructor(props) {
super(props);
this.state = {
store1: ///bind here based on some getter
};
}
render() {
return <ComposedComponent {...this.props} {...this.state} {...this.context} />;
}
};
}
//higher-order.jsx
export default function(ChildComp) {
@withMui //provide store bindings
return class HOC extends Component {
static childContextTypes = {
getAvatar: PropTypes.func
};
getChildContext() {
let {store1} = this.props;
return {
getAvatar: (id) => ({ avatar: store1[id] });
};
}
}
}
//child.js
export default Child extends Component {
static contextTypes = {
getAvatar: PropTypes.func.isRequired
};
handleClick(id, e) {
let {getAvatar} = this.context;
getAvatar(`user_${id}`);
}
render() {
let buttons = [1,2,3].map((id) => {
return <button type="text" onClick={this.handleClick.bind(this, id)}>Click Me</button>
});
return <div>{buttons}</div>;
}
}
//index.jsx
import HOC from './higher-order';
import Child from './child';
let MyComponent = HOC(Child);
React.render(<MyComponent {...anyProps} />, document.body);
然后当你想测试时,你可以很容易地"rewire"你的商店由装饰器提供,因为装饰器在导出的高阶组件内;
//spec.js
import HOC from 'higher-order-component';
import Child from 'child';
describe('rewire the state', () => {
let mockedMuiDecorator = function withMUI(ComposedComponent) {
return class withMUI extends Component {
constructor(props) {
super(props);
this.state = {
store1: ///mock that state here to be passed as props
};
}
render() {
//....
}
}
}
HOC.__Rewire__('withMui', mockedMuiDecorator);
let MyComponent = HOC(Child);
let child = TestUtils.renderIntoDocument(
<MyComponent {...mockedProps} />
);
let childElem = React.findDOMNode(child);
let buttons = childElem.querySelectorAll('button');
it('Should render 3 buttons', () => {
expect(buttons.length).to.equal(3);
});
});
我很确定这并没有真正回答您最初的问题,但我认为您在协调何时使用装饰器时遇到问题 vs.higher 订购组件。
这里有一些很好的资源:
- http://jaysoo.ca/2015/06/09/react-contexts-and-dependency-injection/
- https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750
- https://github.com/badsyntax/react-seed/blob/master/app/components/Menu/tests/Menu-test.jsx
- https://github.com/Yomguithereal/baobab-react/blob/master/test/suites/higher-order.jsx
您可以使用 'babel-plugin-remove-decorators' 插件。此解决方案将让您正常编写组件,而无需导出装饰和未装饰的组件。
先安装插件,然后创建一个包含以下内容的文件,我们称之为'babelTestingHook.js'
require('babel/register')({
'stage': 2,
'optional': [
'es7.classProperties',
'es7.decorators',
// or Whatever configs you have
.....
],
'plugins': ['babel-plugin-remove-decorators:before']
});
和运行你的下面的测试将忽略装饰器,你将能够正常测试组件
mocha ./tests/**/*.spec.js --require ./babelTestingHook.js --recursive
在我的例子中,装饰器非常有用,我不想在我的应用程序中摆脱它们(或 return 包装和未包装的版本)。
我认为最好的方法是使用 Qusai 说的 babel-plugin-remove-decorators
(可用于在测试中删除它们),但我编写的预处理器不同,如下所示:
'use strict';
var babel = require('babel-core');
module.exports = {
process: function(src, filename) {
// Ignore files other than .js, .es, .jsx or .es6
if (!babel.canCompile(filename)) {
return '';
}
if (filename.indexOf('node_modules') === -1) {
return babel.transform(src, {
filename: filename,
plugins: ['babel-plugin-remove-decorators:before']
}).code;
}
return src;
}
};
注意 babel.transform
调用,我将 babel-plugin-remove-decorators:before
元素作为数组值传递,请参阅:https://babeljs.io/docs/usage/options/
要将其与 Jest(我使用的是它)连接起来,您可以在 package.json
:
"jest": {
"rootDir": "./src",
"scriptPreprocessor": "../preprocessor.js",
"unmockedModulePathPatterns": [
"fbjs",
"react"
]
},
其中 preprocessor.js
是预处理器的名称。
使用 Enzyme 测试高阶/浅层装饰器 使用名为 dive()
的方法关注此 link,了解潜水的工作原理
https://github.com/airbnb/enzyme/blob/master/docs/api/ShallowWrapper/dive.md
所以你可以浅化更高阶的组件,然后深入内部。
在上面的例子中:
const wrapper=shallow(<PlayerProfile name={name} avatar={}/>)
expect(wrapper.find("PlayerProfile").dive().find(".player-profile").length).toBe(1)
同样,您可以访问属性并对其进行测试。