React 测试驱动开发:从用户故事到生产
已发表: 2022-03-11在这篇文章中,我们将使用从用户故事到开发的测试驱动开发 (TDD) 来开发一个 React 应用程序。 此外,我们将在 TDD 中使用 Jest 和 Enzyme。 完成本指南后,您将能够:
- 根据需求创建史诗和用户故事。
- 根据用户故事创建测试。
- 使用 TDD 开发 React 应用程序。
- 使用 Enzyme 和 Jest 测试一个 React 应用程序。
- 使用/重用 CSS 变量进行响应式设计。
- 创建一个可重用的 React 组件,该组件根据提供的 props 以不同的方式呈现和运行。
- 使用 React PropTypes 类型检查组件道具。
本文假设您具有 React 的基本知识。 如果您是 React 的新手,我建议您完成官方教程并查看 Toptal 的 2019 React 教程:第 1 部分和第 2 部分。
我们的测试驱动的 React 应用程序概述
我们将构建一个包含一些 UI 组件的基本番茄钟应用程序。 每个组件在相应的测试文件中都有一组单独的测试。 首先,我们可以根据我们的项目需求创建如下的史诗和用户故事。
史诗 | 用户的故事 | 验收标准 |
作为用户,我需要使用计时器来管理我的时间。 | 作为用户,我需要启动计时器以便倒计时。 | 确保用户能够: *启动计时器 *看计时器开始倒计时 即使用户多次单击开始按钮,倒计时也不应中断。 |
作为用户,我需要停止计时器,以便仅在需要时才可以倒计时。 | 确保用户能够: *停止计时器 *看到计时器停止 即使用户多次单击停止按钮,也不会发生任何事情。 | |
作为用户,我需要重置计时器,以便从头开始倒计时。 | 确保用户能够: *重置计时器 *见定时器重置为默认值 |
线框
项目设置
首先,我们将使用Create React App创建一个 React 项目,如下所示:
$ npx create-react-app react-timer $ cd react-timer $ npm start
您将看到在 URL 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 流程如下所示:
- 添加一个测试。
- 运行所有测试,您将看到测试失败。
- 编写代码以通过测试。
- 运行所有测试。
- 重构。
- 重复。
因此,我们将为浅渲染测试添加第一个测试,然后编写代码以通过测试。 将一个名为App.spec.js的新规范文件添加到src/components/App目录,如下所示:
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
第一个测试现在应该通过了。
添加应用 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()
添加定时器组件
最后,应用程序将包含 Timer 组件,因此我们将更新App.spec.js文件以检查应用程序中是否存在 Timer 组件。 此外,我们将在第一个测试用例之外声明容器变量,因为需要在每个测试用例之前完成浅渲染测试。
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 组件,因为我们还没有在 app 组件中添加 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%; } }
此外,我们将在components/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文件以检查Timer组件中是否存在TimerButton组件:
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的 render 方法中添加三个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> ); };
定时器按钮 CSS
现在,是时候为 TimerButton 组件添加 CSS 变量了。 让我们将:root范围内的变量添加到index.css文件中:
: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> );
现在,测试应该通过了。
接下来,我们将添加更多测试来检查挂载的 Timer测试用例中调用每个函数时的状态:
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 开发的知识。 如果您选择模拟这种方法,请参阅此处的完整源代码。