如何在 React Router 4 下对组件进行单元测试
How to unit test a component under React Router 4
我有一个组件,我想测试它的行为:
import React from 'react';
import {connect} from 'react-redux';
import {getModules, setModulesFetching} from 'js/actions/modules';
import {setModuleSort} from 'js/actions/sort';
import Loading from 'js/Components/Loading/Loading';
import Module from './Components/ModuleListModule';
export class ModulesList extends React.Component {
componentDidMount() {
this.props.setModulesFetching(true);
this.props.getModules();
}
renderModuleList() {
if (this.props.isFetching) {
return <Loading/>;
}
if (this._isSelected('name')) {
this.props.modules.sort((a, b) => this._compareNames(a, b));
} else if (this._isSelected('rating')) {
this.props.modules.sort((a, b) => this._compareRatings(a, b));
}
return this.props.modules.map((module) =>
<Module key={module.id} module={module}/>
);
}
renderSelect() {
return (
<div className="col">
<label htmlFor="search-sortby">Sort By:</label>
<select id="search-sortby" onChange={(event) => this.props.setModuleSort(event.target.value)}>
<option value="name" selected={this._isSelected('name')}>Name</option>
<option value="rating" selected={this._isSelected('rating')}>Rating</option>
</select>
</div>
);
}
render() {
return (
<div id="modules-list">
<div className="p-3 row">
{this.renderSelect()}
<div id="search-summary">
{this.props.modules.length} Adventures Found
</div>
</div>
{this.renderModuleList()}
</div>
);
}
// Other minor methods left out for brevity
}
<Module/>
子组件正在与导致问题的 React Router 交互:
import React from 'react'
import {Link} from 'react-router-dom'
import ModuleStarRating from 'js/Components/ModuleRating/ModuleStarRating'
class ModuleListModule extends React.Component {
// Once again, unrelated content omitted
render() {
return (
<Link to={this.moduleLink()}>
<div className="module">
<div className="p-2 row">
<div className="col">
<h5>{this.props.module.name}</h5>
<div className="module-subheader">
{this.props.module.edition.name} {this.renderLevel()} {this.renderLength()}
</div>
<div className="module-summary">{this.props.module.summary}</div>
</div>
<div className="col-2 text-center">
<ModuleStarRating current={this.currentRating()} readonly/>
</div>
<div className="col-2 text-center">
<img src={this.props.module.small_cover}/>
</div>
</div>
</div>
</Link>
)
}
}
根据 React Router docs,我应该在测试中使用 <MemoryRouter/>
来绕过路由器功能。
import {MemoryRouter} from 'react-router-dom';
import {ModulesList} from 'js/Scenes/Home/ModulesList';
import React from 'react';
import {assert} from 'chai';
import {shallow} from 'enzyme';
import sinon from 'sinon';
describe('ModulesList', () => {
it('should sort by name, when requested.', () => {
const initialProps = {
getModules: sinon.stub(),
isFetching: false,
modules: [],
setModulesFetching: sinon.stub(),
sortBy: 'name'
};
const wrapper = shallow(
<MemoryRouter>
<ModulesList {...initialProps}/>
</MemoryRouter>
);
const nextModules = [
{
avg_rating: [{
aggregate: 1.0
}],
edition: {
name: "fakeedition"
},
id: 0,
name: "Z module"
},
{
avg_rating: [{
aggregate: 1.0
}],
edition: {
name: "fakeedition"
},
id: 1,
name: "Y module"
}
];
wrapper.setProps({modules: nextModules});
assert.equal(wrapper.render().find('#search-summary').text(), '3 Adventures Found');
});
});
但是这个测试失败了。调用酶的 ShallowWrapper.setProps()
不会产生预期的效果,因为道具没有应用于我的组件,它们被应用于 <MemoryRouter/>
。但是我不能省略 <MemoryRouter/>
因为如果我这样做,我会得到这个错误:
Warning: Failed context type: The context `router` is marked as required in `Link`, but its value is `undefined`.
in Link (created by ModuleListModule)
in ModuleListModule
in div
TypeError: Cannot read property 'history' of undefined
How can I setup my test such that I can call setProps()
and the component actually updates?
研究了几天,想出了一个解决方案。我选择了 a third party library 来模拟 Redux 商店,但其余部分是我通过谷歌搜索拼凑而成的。
/* global describe, it */
import {MemoryRouter} from 'react-router-dom';
import {ModulesList} from 'js/Scenes/Home/ModulesList';
import {Provider} from 'react-redux';
import React from 'react';
import {assert} from 'chai';
import configureStore from 'redux-mock-store';
import {shallow} from 'enzyme';
import sinon from 'sinon';
describe('ModulesList' () => {
it('should sort by name, when requested.', () => {
const storeFactory = configureStore([]);
const store = storeFactory({});
# This is the harness my component needs in order to function in the
# test environment.
const TestModulesList = (props) => {
return (
<Provider store={store}>
<MemoryRouter>
<ModulesList {...props}/>
</MemoryRouter>
</Provider>
);
};
const initialProps = {
getModules: sinon.stub(),
isFetching: false,
modules: [],
setModulesFetching: sinon.stub(),
sortBy: 'name'
};
const wrapper = shallow(<TestModulesList {...initialProps}/>);
const nextModules = [/* omitted */];
wrapper.setProps({modules: nextModules});
assert.equal(wrapper.render().find('#search-summary').text(), '2 Adventures Found');
});
});
我有一个组件,我想测试它的行为:
import React from 'react';
import {connect} from 'react-redux';
import {getModules, setModulesFetching} from 'js/actions/modules';
import {setModuleSort} from 'js/actions/sort';
import Loading from 'js/Components/Loading/Loading';
import Module from './Components/ModuleListModule';
export class ModulesList extends React.Component {
componentDidMount() {
this.props.setModulesFetching(true);
this.props.getModules();
}
renderModuleList() {
if (this.props.isFetching) {
return <Loading/>;
}
if (this._isSelected('name')) {
this.props.modules.sort((a, b) => this._compareNames(a, b));
} else if (this._isSelected('rating')) {
this.props.modules.sort((a, b) => this._compareRatings(a, b));
}
return this.props.modules.map((module) =>
<Module key={module.id} module={module}/>
);
}
renderSelect() {
return (
<div className="col">
<label htmlFor="search-sortby">Sort By:</label>
<select id="search-sortby" onChange={(event) => this.props.setModuleSort(event.target.value)}>
<option value="name" selected={this._isSelected('name')}>Name</option>
<option value="rating" selected={this._isSelected('rating')}>Rating</option>
</select>
</div>
);
}
render() {
return (
<div id="modules-list">
<div className="p-3 row">
{this.renderSelect()}
<div id="search-summary">
{this.props.modules.length} Adventures Found
</div>
</div>
{this.renderModuleList()}
</div>
);
}
// Other minor methods left out for brevity
}
<Module/>
子组件正在与导致问题的 React Router 交互:
import React from 'react'
import {Link} from 'react-router-dom'
import ModuleStarRating from 'js/Components/ModuleRating/ModuleStarRating'
class ModuleListModule extends React.Component {
// Once again, unrelated content omitted
render() {
return (
<Link to={this.moduleLink()}>
<div className="module">
<div className="p-2 row">
<div className="col">
<h5>{this.props.module.name}</h5>
<div className="module-subheader">
{this.props.module.edition.name} {this.renderLevel()} {this.renderLength()}
</div>
<div className="module-summary">{this.props.module.summary}</div>
</div>
<div className="col-2 text-center">
<ModuleStarRating current={this.currentRating()} readonly/>
</div>
<div className="col-2 text-center">
<img src={this.props.module.small_cover}/>
</div>
</div>
</div>
</Link>
)
}
}
根据 React Router docs,我应该在测试中使用 <MemoryRouter/>
来绕过路由器功能。
import {MemoryRouter} from 'react-router-dom';
import {ModulesList} from 'js/Scenes/Home/ModulesList';
import React from 'react';
import {assert} from 'chai';
import {shallow} from 'enzyme';
import sinon from 'sinon';
describe('ModulesList', () => {
it('should sort by name, when requested.', () => {
const initialProps = {
getModules: sinon.stub(),
isFetching: false,
modules: [],
setModulesFetching: sinon.stub(),
sortBy: 'name'
};
const wrapper = shallow(
<MemoryRouter>
<ModulesList {...initialProps}/>
</MemoryRouter>
);
const nextModules = [
{
avg_rating: [{
aggregate: 1.0
}],
edition: {
name: "fakeedition"
},
id: 0,
name: "Z module"
},
{
avg_rating: [{
aggregate: 1.0
}],
edition: {
name: "fakeedition"
},
id: 1,
name: "Y module"
}
];
wrapper.setProps({modules: nextModules});
assert.equal(wrapper.render().find('#search-summary').text(), '3 Adventures Found');
});
});
但是这个测试失败了。调用酶的 ShallowWrapper.setProps()
不会产生预期的效果,因为道具没有应用于我的组件,它们被应用于 <MemoryRouter/>
。但是我不能省略 <MemoryRouter/>
因为如果我这样做,我会得到这个错误:
Warning: Failed context type: The context `router` is marked as required in `Link`, but its value is `undefined`.
in Link (created by ModuleListModule)
in ModuleListModule
in div
TypeError: Cannot read property 'history' of undefined
How can I setup my test such that I can call
setProps()
and the component actually updates?
研究了几天,想出了一个解决方案。我选择了 a third party library 来模拟 Redux 商店,但其余部分是我通过谷歌搜索拼凑而成的。
/* global describe, it */
import {MemoryRouter} from 'react-router-dom';
import {ModulesList} from 'js/Scenes/Home/ModulesList';
import {Provider} from 'react-redux';
import React from 'react';
import {assert} from 'chai';
import configureStore from 'redux-mock-store';
import {shallow} from 'enzyme';
import sinon from 'sinon';
describe('ModulesList' () => {
it('should sort by name, when requested.', () => {
const storeFactory = configureStore([]);
const store = storeFactory({});
# This is the harness my component needs in order to function in the
# test environment.
const TestModulesList = (props) => {
return (
<Provider store={store}>
<MemoryRouter>
<ModulesList {...props}/>
</MemoryRouter>
</Provider>
);
};
const initialProps = {
getModules: sinon.stub(),
isFetching: false,
modules: [],
setModulesFetching: sinon.stub(),
sortBy: 'name'
};
const wrapper = shallow(<TestModulesList {...initialProps}/>);
const nextModules = [/* omitted */];
wrapper.setProps({modules: nextModules});
assert.equal(wrapper.render().find('#search-summary').text(), '2 Adventures Found');
});
});