React测试驱动的开发:从用户故事到生产
在这篇文章中,我们将使用测试驱动开发(TDD)开发一个React应用,从用户故事到开发。同时,我们将使用Jest和Enzyme来进行TDD。完成本指南后,你将能够:
- 在需求的基础上创建史诗和用户故事。
- 基于用户故事创建测试。
- 使用TDD开发一个React应用程序。
- 使用Enzyme和Jest来测试React应用程序。
- 使用/重复使用CSS变量进行响应式设计。
- 创建一个可重用的React组件,根据所提供的props进行不同的渲染和功能。
- 使用React PropTypes类型检查组件的道具。
我们的测试驱动的React应用程序的概述
我们将建立一个由一些UI组件组成的基本绒球定时器应用。每个组件在相应的测试文件中都会有一组单独的测试。首先,我们可以根据我们的项目要求,创建如下的史诗和用户故事。
| EPIC | 用户故事 | 接受标准 |
| 作为一个用户,我需要使用计时器,以便我可以管理我的时间。 | 作为一个用户,我需要启动计时器,以便我可以倒计时。 | 确保用户能够: *启动定时器 *看到定时器开始倒计时 即使用户多次点击启动按钮,倒计时也不应被打断。 |
| 作为一个用户,我需要停止定时器,这样我才能在需要的时候倒数我的时间。 | 确保用户能够: *停止定时器 *看到定时器停止 即使用户点击停止按钮超过一次,也不应发生任何事情。 | |
| 作为一个用户,我需要重新设置定时器,这样我就可以从头开始倒计时。 | 确保用户能够: *重置定时器 *看到定时器重置为默认值 |
线框图
项目设置
首先,我们要用Create React App创建一个React项目,如下所示。
$ npx create-react-app react-timer
$ cd react-timer
$ npm start
你会看到一个新的浏览器标签打开,网址是http://localhost:3000。你可以使用Ctrl+C停止正在运行的React应用程序。
现在,我们要添加Jest和Enzyme以及一些依赖项,如下所示。
$ npm i -D enzyme
$ npm i -D react-test-renderer enzyme-adapter-react-16
另外,我们将在src目录下添加或更新一个名为setupTests.js的文件。
import { configure } from ‘enzyme’;
import Adapter from ‘enzyme-adapter-react-16’;
configure({ adapter: new Adapter() });
由于Create React App在每次测试前都会运行setupTests.js文件,它将执行并正确配置Enzyme。
配置CSS
我们将编写变量和一个基本的CSS重置,因为我们希望CSS变量在应用程序中全局可用。我们将在:root范围内定义这些变量。定义变量的语法是使用自定义的属性符号,每个变量以"-"开头,后面是变量名称。
导航到index.css文件并添加以下内容。
:root {
--main-font: “Roboto”, sans-serif;
}
body, div, p {
margin: 0;
padding: 0;
}
现在,我们需要将CSS导入我们的应用程序。更新index.js文件,如下所示。
import React from ‘react’;
import ReactDOM from ‘react-dom’;
import ‘./index.css’;
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>
document.getElementById(“root”)
)
浅层渲染测试
正如你可能已经知道的,TDD过程会是这样的
- 添加一个测试。
- 运行所有测试,你会看到测试失败。
- 编写代码以通过测试。
- 运行所有的测试。
- 重构。
- 重复。
因此,我们要为浅层渲染测试添加第一个测试,然后编写代码来通过测试。在src/components/App目录下添加一个名为App.spec.js的新 spec 文件,如下所示。
import React from ‘react’;
import { shallow } from ‘enzyme’;
import App from ‘./App’;
describe(‘App’, () => {
it(‘should render a <div />’, () => {
const container = shallow(<App />);
expect(container.find(‘div’).length).toEqual(1);
});
});
然后,你可以运行该测试。
$ npm test
你会看到测试失败。
应用程序组件
现在,我们将继续创建App组件以通过测试。导航到src/components/App目录下的App.jsx,添加如下代码。
import React from ‘react’;
const App = () => <div className=”app-container” />;
export default App;
现在,再次运行测试。
$ npm test
现在第一个测试应该通过了。
添加App的CSS
我们将在src/components/App目录下创建一个App.css文件,为App组件添加一些样式,如下所示。
.app-container {
height: 100vh;
width: 100vw;
align-items: center;
display: flex;
justify-content: center;
}
现在,我们准备将CSS导入到App.jsx文件中。
import React from ‘react’;
import ‘./App.css’;
const App = () => <div className=”app-container” />;
export default App;
接下来,我们必须更新index.js文件以导入App组件,如下所示。
import React from "react"
import ReactDOM from "react-dom"
import "./index.css"
import App from "./components/App/App"
import * as serviceWorker from "./serviceWorker"
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()
添加定时器组件
最后,应用程序将包含定时器组件,因此我们要更新App.spec.js文件以检查我们的应用程序中是否存在定时器组件。另外,我们将在第一个测试案例之外声明容器变量,因为浅层渲染测试需要在每个测试案例之前完成。
import React from "react"
import { shallow } from "enzyme"
import App from "./App"
import Timer from "../Timer/Timer"
describe("App", () => {
let container
beforeEach(() => (container = shallow(<App />)))
it("should render a <div />", () => {
expect(container.find("div").length).toEqual(1)
})
it("should render the Timer Component", () => {
expect(container.containsMatchingElement(<Timer />)).toEqual(true)
})
})
如果你在这个阶段运行npm test ,测试将会失败,因为Timer组件还不存在。
编写定时器浅层渲染测试
现在,我们将在src/components目录下一个名为Timer的新目录中创建一个名为Timer .spec.js的文件。
同时,我们将在Timer.spec.js文件中添加浅层渲染测试。
import React from "react"
import { shallow } from "enzyme"
import Timer from "./Timer"
describe("Timer", () => {
let container
beforeEach(() => (container = shallow(<Timer />)))
it("should render a <div />", () => {
expect(container.find("div").length).toBeGreaterThanOrEqual(1)
})
})
该测试将失败,正如预期的那样。
创建定时器组件
接下来,让我们创建一个名为Timer.jsx的新文件,并根据用户故事定义相同的变量和方法。
import React, { Component } from 'react';
class Timer extends Component {
constructor(props) {
super(props);
this.state = {
minutes: 25,
seconds: 0,
isOn: false
};
}
startTimer() {
console.log('Starting timer.');
}
stopTimer() {
console.log('Stopping timer.');
}
resetTimer() {
console.log('Resetting timer.');
}
render = () => {
return <div className="timer-container" />;
};
}
export default Timer;
这应该能通过测试,并在Timer.spec.js文件中渲染一个<div /> ,但测试不应该渲染Timer组件,因为我们还没有在应用组件中添加Timer组件。
我们要像这样在App.jsx文件中添加Timer组件。
import React from 'react';
import './App.css';
import Timer from '../Timer/Timer';
const App = () => (
<div className="app-container">
<Timer />
</div>
);
export default App;
现在所有的测试都应该通过。
添加定时器的CSS
我们将添加与定时器相关的CSS变量,并为小型设备添加媒体查询。
更新index.css文件,如下所示。
:root {
--timer-background-color: #FFFFFF;
--timer-border: 1px solid #000000;
--timer-height: 70%;
--timer-width: 70%;
}
body, div, p {
margin: 0;
padding: 0;
}
@media screen and (max-width: 1024px) {
:root {
--timer-height: 100%;
--timer-width: 100%;
}
}
另外,我们要在component/Timer目录下创建Timer.css文件。
.timer-container {
background-color: var(--timer-background-color);
border: var(--timer-border);
height: var(--timer-height);
width: var(--timer-width);
}
我们要更新Timer.jsx以导入Timer.css文件。
import React, { Component } from "react"
import "./Timer.css"
如果你现在运行React应用程序,你会在浏览器上看到一个带有边框的简单屏幕。
编写TimerButton的浅层渲染测试
我们需要三个按钮。开始、停止和重置,因此我们要创建TimerButton组件。
首先,我们需要更新Timer.spec.js文件,检查TimerButton组件在Timer组件中是否存在。
it("should render instances of the TimerButton component", () => {
expect(container.find("TimerButton").length).toEqual(3)
})
现在,让我们把TimerButton.spec.js文件添加到src/components目录下一个名为TimerButton的新目录中,然后让我们像这样把测试添加到该文件中。
import React from "react"
import { shallow } from "enzyme"
import TimerButton from "./TimerButton"
describe("TimerButton", () => {
let container
beforeEach(() => {
container = shallow(
<TimerButton
buttonAction={jest.fn()}
buttonValue={""}
/>
)
})
it("should render a <div />", () => {
expect(container.find("div").length).toBeGreaterThanOrEqual(1)
})
})
现在,如果你运行该测试,你会看到测试失败。
让我们为TimerButton组件创建TimerButton.jsx文件。
import React from 'react';
import PropTypes from 'prop-types';
const TimerButton = ({ buttonAction, buttonValue }) => (
<div className="button-container" />
);
TimerButton.propTypes = {
buttonAction: PropTypes.func.isRequired,
buttonValue: PropTypes.string.isRequired,
};
export default TimerButton;
如果你在这个阶段运行npm test ,测试应该呈现TimerButton组件的实例,但会失败,因为我们还没有把TimerButton组件添加到Timer组件中。
让我们导入TimerButton组件并在Timer.jsx的渲染方法中添加三个TimerButton组件。
render = () => {
return (
<div className="timer-container">
<div className="time-display"></div>
<div className="timer-button-container">
<TimerButton buttonAction={this.startTimer} buttonValue={'Start'} />
<TimerButton buttonAction={this.stopTimer} buttonValue={'Stop'} />
<TimerButton buttonAction={this.resetTimer} buttonValue={'Reset'} />
</div>
</div>
);
};
TimerButton CSS
现在,是时候为TimerButton组件添加CSS变量了。让我们在index.css文件的 :root 范围内添加变量。
:root {
...
--button-border: 3px solid #000000;
--button-text-size: 2em;
}
@media screen and (max-width: 1024px) {
:root {
…
--button-text-size: 4em;
}
}
另外,让我们在src/components目录下的TimerButton目录中创建一个名为TimerButton.css的文件。
.button-container {
flex: 1 1 auto;
text-align: center;
margin: 0px 20px;
border: var(--button-border);
font-size: var(--button-text-size);
}
.button-container:hover {
cursor: pointer;
}
让我们相应地更新TimerButton.jsx来导入TimerButton.css文件并显示按钮值。
import React from 'react';
import PropTypes from 'prop-types';
import './TimerButton.css';
const TimerButton = ({ buttonAction, buttonValue }) => (
<div className="button-container">
<p className="button-value">{buttonValue}</p>
</div>
);
TimerButton.propTypes = {
buttonAction: PropTypes.func.isRequired,
buttonValue: PropTypes.string.isRequired,
};
export default TimerButton;
另外,我们还需要更新Timer.css,使三个按钮水平对齐,所以我们也来更新Timer.css文件。
import React from 'react';
import PropTypes from 'prop-types';
import './TimerButton.css';
const TimerButton = ({ buttonAction, buttonValue }) => (
<div className="button-container">
<p className="button-value">{buttonValue}</p>
</div>
);
TimerButton.propTypes = {
buttonAction: PropTypes.func.isRequired,
buttonValue: PropTypes.string.isRequired,
};
export default TimerButton;
如果你现在运行React应用程序,你会看到如下的屏幕。

重构定时器
我们要重构Timer,因为我们要实现startTimer、stopTimer、restartTimer和resetTimer等函数。让我们首先更新Timer.spec.js文件。
describe('mounted Timer', () => {
let container;
beforeEach(() => (container = mount(<Timer />)));
it('invokes startTimer when the start button is clicked', () => {
const spy = jest.spyOn(container.instance(), 'startTimer');
container.instance().forceUpdate();
expect(spy).toHaveBeenCalledTimes(0);
container.find('.start-timer').first().simulate('click');
expect(spy).toHaveBeenCalledTimes(1);
});
it('invokes stopTimer when the stop button is clicked', () => {
const spy = jest.spyOn(container.instance(), 'stopTimer');
container.instance().forceUpdate();
expect(spy).toHaveBeenCalledTimes(0);
container.find('.stop-timer').first().simulate('click');
expect(spy).toHaveBeenCalledTimes(1);
});
it('invokes resetTimer when the reset button is clicked', () => {
const spy = jest.spyOn(container.instance(), 'resetTimer');
container.instance().forceUpdate();
expect(spy).toHaveBeenCalledTimes(0);
container.find('.reset-timer').first().simulate('click');
expect(spy).toHaveBeenCalledTimes(1);
});
});
如果你运行测试,你会看到添加的测试失败,因为我们还没有更新TimerButton组件。让我们更新TimerButton组件,添加点击事件。
const TimerButton = ({ buttonAction, buttonValue }) => (
<div className="button-container" onClick={() => buttonAction()}>
<p className="button-value">{buttonValue}</p>
</div>
);
现在,测试应该通过了。
接下来,我们将添加更多的测试,以检查在加载的定时器测试案例中每个函数被调用时的状态。
it('should change isOn state true when the start button is clicked', () => {
container.instance().forceUpdate();
container.find('.start-timer').first().simulate('click');
expect(container.instance().state.isOn).toEqual(true);
});
it('should change isOn state false when the stop button is clicked', () => {
container.instance().forceUpdate();
container.find('.stop-timer').first().simulate('click');
expect(container.instance().state.isOn).toEqual(false);
});
it('should change isOn state false when the reset button is clicked', () => {
container.instance().forceUpdate();
container.find('.stop-timer').first().simulate('click');
expect(container.instance().state.isOn).toEqual(false);
expect(container.instance().state.minutes).toEqual(25);
expect(container.instance().state.seconds).toEqual(0);
});
如果你运行这些测试,你会看到它们失败,因为我们还没有实现每个方法。所以让我们实现每个函数以通过测试。
startTimer() {
this.setState({ isOn: true });
}
stopTimer() {
this.setState({ isOn: false });
}
resetTimer() {
this.stopTimer();
this.setState({
minutes: 25,
seconds: 0,
});
}
如果你运行它们,你会看到测试通过。现在,让我们实现Timer.jsx中的其余函数。
import React, { Component } from 'react';
import './Timer.css';
import TimerButton from '../TimerButton/TimerButton';
class Timer extends Component {
constructor(props) {
super(props);
this.state = {
minutes: 25,
seconds: 0,
isOn: false,
};
this.startTimer = this.startTimer.bind(this);
this.stopTimer = this.stopTimer.bind(this);
this.resetTimer = this.resetTimer.bind(this);
}
startTimer() {
if (this.state.isOn === true) {
return;
}
this.myInterval = setInterval(() => {
const { seconds, minutes } = this.state;
if (seconds > 0) {
this.setState(({ seconds }) => ({
seconds: seconds - 1,
}));
}
if (seconds === 0) {
if (minutes === 0) {
clearInterval(this.myInterval);
} else {
this.setState(({ minutes }) => ({
minutes: minutes - 1,
seconds: 59,
}));
}
}
}, 1000);
this.setState({ isOn: true });
}
stopTimer() {
clearInterval(this.myInterval);
this.setState({ isOn: false });
}
resetTimer() {
this.stopTimer();
this.setState({
minutes: 25,
seconds: 0,
});
}
render = () => {
const { minutes, seconds } = this.state;
return (
<div className="timer-container">
<div className="time-display">
{minutes}:{seconds < 10 ? `0${seconds}` : seconds}
</div>
<div className="timer-button-container">
<TimerButton
className="start-timer"
buttonAction={this.startTimer}
buttonValue={'Start'}
/>
<TimerButton
className="stop-timer"
buttonAction={this.stopTimer}
buttonValue={'Stop'}
/>
<TimerButton
className="reset-timer"
buttonAction={this.resetTimer}
buttonValue={'Reset'}
/>
</div>
</div>
);
};
}
export default Timer;
你会看到所有的函数都是基于我们之前准备的用户故事工作的。
所以,这就是我们如何使用TDD开发了一个基本的React应用程序。如果用户故事和验收标准更详细,测试用例就可以写得更精确,从而贡献更大。
总结
当使用TDD开发一个应用程序时,不仅要把项目分解成史诗或用户故事,而且要为验收标准做好准备,这是非常重要的。在这篇文章中,我想告诉你如何在React TDD开发中分解项目并使用准备好的验收标准。
尽管有很多与React TDD相关的资源,我希望这篇文章能帮助你了解一些关于使用用户故事的React TDD开发。如果你选择模仿这种方法,请参考这里的完整源代码。
了解基础知识
你如何进行测试驱动开发?
对于测试驱动开发,你必须首先创建一个测试,并确认该测试是否失败。一旦你确认测试失败,就编写代码以通过测试,必要时重构代码。然后为接下来的测试重复所有这些步骤。
在React中我应该测试什么?
编写测试背后的主要原因是确保应用程序能够正常工作。在React中,我们应该测试应用程序的渲染是否正常,有没有错误。此外,我们应该测试输出和状态,以及事件。最后,我们应该像其他技术栈一样测试边缘案例。
测试驱动的开发好吗?
测试驱动开发是有益的,因为它可以减少生产中的错误,提高代码质量,使代码维护更容易。它还为回归测试提供了自动化测试。然而,TDD对于GUI测试来说成本很高,而且过多的TDD会使代码更加复杂。