我通读了文档的单元测试部分,尽管其中有一个如何测试一个哑组件的示例,但并未涉及能够对“智能组件”(使用connect()方法的那些组件)进行单元测试。 事实证明,由于connect()创建的包装器组件,对智能组件进行单元测试会更加复杂。 问题的一部分是通过connect()包装组件需要有一个“存储”属性(或上下文)。
我努力尝试这样做,希望能得到一些反馈,以了解是否有更好的方法来实现它。 而且,如果我所做的最终看起来很合理,我认为我会推销PR以便在单元测试文档中添加一些信息。
首先,我将现有的示例组件放在docs的单元测试部分中,并将其包装在connect()中,以传递来自状态和调度绑定操作创建者的数据:
import React, { PropTypes, Component } from 'react';
import TodoTextInput from './TodoTextInput';
import TodoActions from '../actions/TodoActions';
import connect from 'redux-react';
class Header extends Component {
handleSave(text) {
if (text.length !== 0) {
this.props.addTodo(text);
}
}
render() {
return (
<header className='header'>
<h1>{this.props.numberOfTodos + " Todos"}</h1>
<TodoTextInput newTodo={true}
onSave={this.handleSave.bind(this)}
placeholder='What needs to be done?' />
</header>
);
}
}
export default connect(
(state) => {numberOfTodos: state.todos.length},
TodoActions
)(Header);
在单元测试文件中,它看起来也与示例类似。
import expect from 'expect';
import jsdomReact from '../jsdomReact';
import React from 'react/addons';
import Header from '../../components/Header';
import TodoTextInput from '../../components/TodoTextInput';
const { TestUtils } = React.addons;
/**
* Mock out the top level Redux store with all the required
* methods and have it return the provided state by default.
* <strong i="13">@param</strong> {Object} state State to populate in store
* <strong i="14">@return</strong> {Object} Mock store
*/
function createMockStore(state) {
return {
subscribe: () => {},
dispatch: () => {},
getState: () => {
return {...state};
}
};
}
/**
* Render the Header component with a mock store populated
* with the provided state
* <strong i="15">@param</strong> {Object} storeState State to populate in mock store
* <strong i="16">@return</strong> {Object} Rendered output from component
*/
function setup(storeState) {
let renderer = TestUtils.createRenderer();
renderer.render(<Header store={createMockStore(storeState)} />);
var output = renderer.getRenderedOutput();
return output.refs.wrappedInstance();
}
describe('components', () => {
jsdomReact();
describe('Header', () => {
it('should call call addTodo if length of text is greater than 0', () => {
const output = setup({
todos: [1, 2, 3]
});
var addTodoSpy = expect.spyOn(output.props, 'addTodo');
let input = output.props.children[1];
input.props.onSave('');
expect(addTodoSpy.calls.length).toBe(0);
input.props.onSave('Use Redux');
expect(addTodoSpy.calls.length).toBe(1);
});
});
});
我已经简化了该测试,只是显示了相关部分,但是我不确定的重点是createMockStore
方法。 如果您尝试在不带任何道具的情况下渲染Header组件,则Redux(或react-redux)会引发错误,指出该组件必须具有store
道具或上下文,因为它希望是该道具的子代。 <Provider>
组件。 由于我不想在单元测试中使用它,因此我创建了一种模拟它的方法,并允许测试通过它想要在商店中设置的状态。
我可以从这种方法中看到的好处是,它不仅可以测试组件中的功能,还可以测试传递给connect()的方法的功能。 我可以在这里轻松地编写另一个断言,该断言执行类似于expect(output.props.numberOfTodos).toBe(3)
,以验证我的mapStateToProps
函数是否正在执行我期望的操作。
它的主要缺点是我不得不模拟Redux存储,它并不那么复杂,但是感觉它是Redux内部逻辑的一部分,并且可能会改变。 显然,对于我的单元测试,我已经将这些方法移到了通用的单元测试实用程序文件中,因此,如果存储方法确实发生了变化,则只需要在一个地方修改我的代码即可。
有什么想法吗? 是否还有其他人尝试过对智能组件进行单元测试,并找到了一种更好的做事方法?
另一种选择是也导出(未修饰的)Header类,因此可以单独导入和测试它。
此外,至少在使用Mocha时,也不需要每个文件都需要jsdomReact调用。 这是我的setup.js:
import jsdom from 'jsdom';
import ExecutionEnvironment from 'react/lib/ExecutionEnvironment';
if (!global.document || !global.window) {
global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.window = document.defaultView;
global.navigator = window.navigator;
ExecutionEnvironment.canUseDOM = true;
window.addEventListener('load', () => {
console.log('JSDom setup completed: document, window and navigator are now on global scope.');
});
}
它是通过--require命令行选项加载的: mocha -r babelhook -r test/setup --recursive
(babelhook是对require('babel-core/register')
的调用)。
是的,我可以同时导出Header类和修饰的类,但我希望避免只为单元测试而更改源代码。 这样做会导致我不得不更改所有包含装饰组件的位置(例如import {DecoratedHeader} from './components/Header'
而不是import Header from
。/ components / Header`)。
至于jsdom设置,我纯粹是从文档中复制示例作为显示用例的一种方式,我并没有将其用作主要设置,只是一个示例。
我只是注意到上面的代码示例不完整。 为了直接测试智能组件,您必须有一些实用方法来返回refs.wrappedInstance
而不只是渲染结果,因为这将为您提供连接装饰的组件(在上面的设置函数中更新) 。 这再次使单元测试依赖于Redux内部(在这种情况下,特别是react-redux内部)。 这样就可以了,但是感觉有点脆弱。
@ernieturner我们还getWrappedInstance()
公共API,因此如果您担心直接访问refs
可以依靠它。 还有一些工具,如react-test-tree ,也可以使其透明。
@ernieturner我认为@ghengeveld的意思是,如果您在所有地方都使用ES6模块,则装饰的Header组件仍可以作为默认值导出,而普通的React组件可以作为附加的命名导出。 例如 -
//header.js
export class HeaderDumbComponent {
render() {
return <header><div>...</div></header>;
}
}
export default connect(
(state) => {numberOfTodos: state.todos.length},
TodoActions
)(HeaderDumbComponent);
通过这种双重导出,您的主应用可以像以前一样使用import Header from './header.js'
来使用智能组件,而您的测试可以绕过redux并直接使用import {HeaderDumbComponent} from '../components/header.js'
测试核心组件的行为
@ eugene1g @ghengeveld这实际上是解决问题的一个很好的解决方案。 随时将PR发送到“写作测试”文档中进行解释!
@ eugene1g就是我的意思。
@gaearon我会做到的。
@ghengeveld对误解
export class HeaderDumpComponent ...
export function mapStateToProps(state) ...
export function mapDispatchToProps(dispatch) ...
export default connect(mapStateToProps, mapDispatchToProps)(HeaderDumpComponent);
大多数map方法很可能会足够简单而不必进行单元测试,但是我有一些值得验证,因为它们是我组件功能的关键部分。
感谢大家的建议,如果您需要有关文档的任何帮助,请告诉我。
我们正在尝试上面概述的推荐方法,但是我们对此并不完全满意。 这使传递给connect
的参数在集成测试之外未经测试。 我知道模拟存储可能会很脆弱,但是我觉得如果支持组件测试, redux-mock-store这样
@carpeliam
为什么不只使用常规存储并以特定的初始状态对其进行水合呢?
仅当您要测试正在分派哪些操作时,才需要模拟存储。
@carpeliam
请在此存储库中查看有关counter
和todomvc
示例的单元测试。
我们遇到的一个问题是,我们正在另一个组件内部渲染智能组件,而无法使用浅层渲染器(组件使用componentDid *方法)。 我们所做的是对connect函数进行存根(使用sinon)以返回一个返回简单React组件的函数。
它很脆弱,我们希望一旦可以迁移到React 0.14即可迁移到较浅的渲染器,但是这种方法目前在我们的测试中不受阻碍。
我也有麻烦。 我对React和Redux都比较陌生,所以这可能使我退缩。 您如何解决它? @songawee
建议的双重导出方法使select(mapStoreToState)函数未经测试。 为了独立测试它,还需要将其导出,这是测试名称的又一变化。
我很想找到一种方法来使shallowRenderer与connect的包装组件一起使用。 我当前的麻烦是使用浅层渲染器时,它只能传递回预期的Connect组件。
建议的双重导出方法使select(mapStoreToState)函数未经测试。 为了独立测试它,还需要将其导出,这是测试名称的又一变化。
并非严格“以测试的名义”。 实际上,我们建议您在简化器旁边定义选择器函数(最终是mapStateToProps
),并一起测试它们。 此逻辑独立于UI,并且不必与UI捆绑在一起。 看一下一些选择器的shopping-cart
示例。
那么@gaearon ,您是否建议使用此_selector_函数从状态中获取所有数据? 难道这不会带来很多不必要的开销,因为在大多数情况下,人们只会从状态中读取一堆属性并将它们分配给组件上的props?
是的,Redux的一般建议模式是在几乎所有地方都使用选择器功能,而不是直接访问state.some.nested.field
。 它们可以是非常简单的“普通”功能,但是最常见的是使用提供备注功能的Reselect库组合在一起。
为什么会产生任何额外的开销?
我一直在执行此处所述的双重导出,但是阅读了connect
源,我意识到“ Container”在WrappedComponent
静态属性中保留了对“ dumb”组件的引用:
所以代替:
// header.js
export const HeaderDumbComponent = (props) => <header><div>...</div></header>
export default connect(mapStateToProps)(HeaderDumbComponent)
// header.spec.js
import { HeaderDumbComponent } from './header'
it('renders', () => {
expect(<HeaderDumbComponent />).to.not.be.null
})
您可以在需要“哑巴”版本的地方使用WrappedComponent
来避免双重导出:
// header.js
const HeaderDumbComponent = (props) => <header><div>...</div></header>
export default connect(mapStateToProps)(HeaderDumbComponent)
// header.spec.js
import Header from './header'
it('renders', () => {
expect(<Header.WrappedComponent />).to.not.be.null
})
@gaearon - WrappedComponent
似乎是一个非常稳定的属性,因为它位于文档中。 您是否出于任何原因建议不要以这种方式使用它?
我要做的是导出未包装的组件,还导出mapStateToProps和mapDispatchToProps,以便可以对这些函数进行单元测试。 这是一个例子:
import {ManageCoursePage, mapStateToProps, mapDispatchToProps} from './ManageCoursePage';
describe("mapStateToProps", () => {
function setup(courses, authors, id) {
const state = {
authors: authors
};
const ownProps = {
params: {
id: id
}
};
return mapStateToProps(state, ownProps);
}
it('sets the course when a course id is set', () => {
const courses = [ { id: "1", foo: "bar" }, { id: "2", foo: "notbar" } ];
const authors = [];
const id = "1";
const props = setup(courses, authors, id);
expect(props.course).toBe(courses[0]);
});
it('sets the course when a course id is not set', () => {
const courses = [ { id: "1", foo: "bar" }, { id: "2", foo: "notbar" } ];
const authors = [];
const props = setup(courses, authors, null);
expect(props.course).toEqual({});
});
it('sets the course when a course id is set that is not present in the courses list', () => {
const courses = [ { id: "1", foo: "bar" }, { id: "2", foo: "notbar" } ];
const authors = [];
const id = "42";
const props = setup(courses, authors, null);
expect(props.course).toEqual({});
});
it('sets the authors formatted for a drop down list', () => {
const courses = [];
const authors = [ { id: "1", name: "John" }, { id: "2", name: "Jill" }];
const props = setup(courses, authors, null);
expect(props.authors).toEqual([
{ value: "1", text: "John" },
{ value: "2", text: "Jill" }
])
});
});
最有用的评论
@ernieturner我认为@ghengeveld的意思是,如果您在所有地方都使用ES6模块,则装饰的Header组件仍可以作为默认值导出,而普通的React组件可以作为附加的命名导出。 例如 -
通过这种双重导出,您的主应用可以像以前一样使用
import Header from './header.js'
来使用智能组件,而您的测试可以绕过redux并直接使用import {HeaderDumbComponent} from '../components/header.js'
测试核心组件的行为