[译] React 测试驱动开发:从用户故事到产品

简介: [译] React 测试驱动开发:从用户故事到产品

原文:www.toptal.com/react/tdd-r…


在本文中,我们将采用 测试驱动开发(TDD:test-driven development) 方法,从用户故事到产品开发一个 React 应用。同时,我们将在 TDD 中使用 Jest 和 Enzyme 。一旦完成本教程,你将能够:

  • 基于需求创建 epic 和 user stories(用户故事)
  • 基于用户故事创建测试
  • 使用 TDD 开发一个 React 应用
  • 使用 Enzyme 和 Jest 测试 React 应用
  • 使用/复用 CSS variables 实现响应式设计
  • 创建一个根据所提供的 props 实现不同渲染和功能的可复用 React 组件
  • 使用 React PropTypes 实现组件 props 的类型检查

译注: epic(史诗)、user stories(用户故事)、acceptance criteria(验收准则)都是敏捷式开发中的相关概念

本文假设你已经具备了 React 和单元测试的基本知识,如果有必要请参阅如下资料:

应用概览

我们将创建一个由某些 UI 组件构成的番茄计时器基础应用。每一个组件都会在相关的一个测试文件中拥有独立的一组测试。首先,我们可以基于项目需求创建如下的史诗和用户故事:

史诗 用户故事 验收准则
作为一个用户,我需要使用计时器以管理时间 作为一个用户,我要能启动计时器以开始倒计时。 确保用户能够:

*启动计时器
*看到计时器开始倒计时

即便用户多次点击启动按钮,倒计时也不应被中断
作为一个用户,我要能停止计时器,这样只有在我需要时才会倒计时。 确保用户能够:

*停止计时器
*看到计时器被停止了

当用户多次点击停止按钮后,不应该再发生什么
作为一个用户,我要能重置计时器,这样我又能从头开始倒计时了。 确保用户能够:

*重置计时器
*看到时间被重置为默认状态

线框图

项目设置

首先,我们使用 Create React App 创建如下这样的一个 React 项目:

js


$ npx create-react-app react-timer
$ cd react-timer
$ npm start

你将看到浏览器的一个新 tab 页被打开,其 URL 为 http://localhost:3000 。可以按下 Ctrl+C 结束这个 React 应用的运行。

现在,将 Jest 和 Enzyme 加入依赖:

js

$ npm i -D enzyme
$ npm i -D react-test-renderer enzyme-adapter-react-16

同时,我们要添加或更新 src 目录中的 setupTests.js 文件:

js



import { configure } from ‘enzyme’;
import Adapter from ‘enzyme-adapter-react-16;
configure({ adapter: new Adapter() });

因为 Create React App 会在每个测试之前运行 setupTests.js 文件,故这将正确地配置好 Enzyme。

配置 CSS

我们来编写基础的 CSS reset,因为想让 CSS variables 在应用中全局可用,也将在 :root 作用域中定义一些变量。定义变量的语法是使用自定义属性符,每个变量名都由 -- 开头。

打开 index.css 文件并添加如下内容:

js

:root {
  --main-font: “Roboto”, sans-serif;
}
body, div, p {
  margin: 0;
  padding: 0;
}

现在,需要将该 CSS 导入应用。将 index.js 更新为:

js

import React from ‘react’;
import ReactDOM from ‘react-dom’;
import ./index.css’;
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
  document.getElementById(“root”)
)

浅渲染测试

正如你或许已经知道的,TDD 过程可能看起来像这样:

  1. 添加一个测试
  2. 运行所有测试,不出所料的失败
  3. 编写代码以通过测试
  4. 再次运行所有测试
  5. 重构代码
  6. 周而复始

因此,我们先添加一个浅渲染(shallow render)的测试,并编写代码使其通过。向 src/components/App 目录中添加一个名为 App.spec.js 的规格文件,如下:

js

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);
  });
});

然后运行测试:

js

$ npm test

你会看到测试失败。

添加组件

接下来创建 App 组件以通过测试。打开 src/components/App/App.jsx 并添加如下代码:

js

import React from ‘react’;
const App = () => <div className=”app-container” />;
export default App;

再次运行测试,首个测试将通过。

添加 App 的样式

接下来我们在 src/components/App 目录中创建一个 App.css 文件,增加一些 App 组件的样式:

css

.app-container {
  height: 100vh;
  width: 100vw;
  align-items: center;
  display: flex;
  justify-content: center;
}

将其引入 App.jsx 文件:

js

import React from ‘react’;
import ./App.css’;
const App = () => <div className=”app-container” />;
export default App;

下一步,更新 index.js 文件,增加引入 App 组件的逻辑:

js

import React from "react"
import ReactDOM from "react-dom"
import "./index.css"
import App from "./components/App/App"
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
)

添加计时器组件

最后,应用得有个计时器组件,因此我们来更新 App.spec.js 文件用以检查其存在。同时,将变量 container 声明在首个测试用例之外,这样在每个测试用例之前都能用到浅渲染了。

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 组件的浅渲染测试:

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 组件

下一步,创建名为 Timer.jsx 的新文件,并基于用户故事定义相同的变量和方法:

js


import React, { Component } from 'react';
class Timer extends Component {
  constructor(props) {
    super(props);
    this.state = {
      minutes: 25,
      seconds: 0,
        isOn: false
    };
  }
  startTimer() {
    console.log('启动定时器');
  }
  stopTimer() {
    console.log('停止定时器');
  }
  resetTimer() {
    console.log('重置定时器');
  }
  render = () => {
    return <div className="timer-container" />;
  };
}
export default Timer;

这将在 Timer.spec.js 中的测试用例中渲染一个 <div /> 并使之通过,然而  App.spec.js  仍会失败,因为我们尚未把 Timer 组件加入 App 中。

更新 App.jsx 文件:

js


import React from 'react';
import './App.css';
import Timer from '../Timer/Timer';
const App = () => (
  <div className="app-container">
    <Timer />
  </div>
);
export default App;

现在所有测试都通过了。

为 Timer 增加样式

增加计时器相关的 CSS variables 以及适配小尺寸设备的媒体查询。

index.css 更新为:

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

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 文件。

js

import React, { Component } from "react"
import "./Timer.css"

至此如果你运行这个 React 应用,将看到浏览器中出现一个带有边框的简单屏幕区域了。

编写 TimerButton 测试用例

我们需要三个按钮:Start、* Stop* 和 Reset,因此要创建一个 TimerButton 组件。

首先,更新 Timer.spec.js 文件以检查 Timer 组件中几个按钮的存在:

js

it("should render instances of the TimerButton component", () => {
    expect(container.find("TimerButton").length).toEqual(3)
})

现在,在 src/components 目录下建立子目录 TimerButton 并添加 TimerButton.spec.js 文件,在其中编写如下测试:

js

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.jsx 文件:

js

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;

Timer.jsx 中引入并添加三个 TimerButton 组件:

js

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 的样式

现在轮到为 TimerButton 组件增加 CSS variables 了。把 index.css 文件更新为:

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 的文件:

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 中引入样式,并显示按钮 value :

js

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 以在底部横向排列三个按钮:

css

...
.time-display {
  height: 70%;
  font-size: 5em;
  display: flex;
  justify-content: center;
  margin-left: auto;
  flex-direction: column;
  align-items: center;
}
.timer-button-container {
  display: flex;
  flex-direction: row;
  justify-content: center;
  height: 30%;
}

如果现在运行这个 React 应用,将看到如下的效果:

重构 Timer

为了实现 启动定时器停止定时器重置定时器 等功能,需要对 Timer 重构。先来更新 Timer.spec.js 测试:

js

describe('mounted Timer', () => {
  let container;
  beforeEach(() => (container = mount(<Timer />)));
  it('点击 Start 按钮时调用 startTimer 方法', () => {
    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('点击 Stop 按钮时调用 stopTimer 方法', () => {
    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('点击 Reset 按钮时调用 resetTimer 方法', () => {
    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 组件中更新相关功能。让我们来添加点击的功能:

js

const TimerButton = ({ buttonAction, buttonValue }) => (
  <div className="button-container" onClick={() => buttonAction()}>
    <p className="button-value">{buttonValue}</p>
  </div>
);

测试现在会通过了。

下一步,添加更多的测试用例以检查每个方法被调用后组件的状态:

js

it('点击 Start 按钮后状态 isOn 应变为 true', () => {
    container.instance().forceUpdate();
    container.find('.start-timer').first().simulate('click');
    expect(container.instance().state.isOn).toEqual(true);
  });
  it('点击 Stop 按钮后状态 isOn 应变为 false', () => {
    container.instance().forceUpdate();
    container.find('.stop-timer').first().simulate('click');
    expect(container.instance().state.isOn).toEqual(false);
  });
  it('点击 Reset 按钮后状态 isOn 应变为 false 等', () => {
    container.instance().forceUpdate();
    container.find('.reset-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);
 });

因为还未实现每个方法,所以测试将会失败。更新组件为:

js

startTimer() {
    this.setState({ isOn: true });
  }
  stopTimer() {
    this.setState({ isOn: false });
  }
  resetTimer() {
    this.stopTimer();
    this.setState({
      minutes: 25,
      seconds: 0,
    });
}

现在测试可以通过了。让我们实现 Timer.jsx 的剩余功能吧:

js

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 开发的帮助。

示例源代码可在这里找到:github.com/hyungmoklee…



相关文章
|
13天前
|
安全 Linux 虚拟化
|
1月前
|
机器学习/深度学习 人工智能 监控
提升软件质量的关键路径:高效测试策略与实践在软件开发的宇宙中,每一行代码都如同星辰般璀璨,而将这些星辰编织成星系的过程,则依赖于严谨而高效的测试策略。本文将引领读者探索软件测试的奥秘,揭示如何通过精心设计的测试方案,不仅提升软件的性能与稳定性,还能加速产品上市的步伐,最终实现质量与效率的双重飞跃。
在软件工程的浩瀚星海中,测试不仅是发现缺陷的放大镜,更是保障软件质量的坚固防线。本文旨在探讨一种高效且创新的软件测试策略框架,它融合了传统方法的精髓与现代技术的突破,旨在为软件开发团队提供一套系统化、可执行性强的测试指引。我们将从测试规划的起点出发,沿着测试设计、执行、反馈再到持续优化的轨迹,逐步展开论述。每一步都强调实用性与前瞻性相结合,确保测试活动能够紧跟软件开发的步伐,及时适应变化,有效应对各种挑战。
|
16天前
|
前端开发 JavaScript 安全
学习如何为 React 组件编写测试:
学习如何为 React 组件编写测试:
33 2
|
18天前
|
前端开发 JavaScript 测试技术
React 模拟测试与 Jest
【10月更文挑战第21天】本文介绍了如何使用 Jest 进行 React 组件的单元测试和模拟测试,涵盖了基础概念、常见问题及解决方案,并提供了实践案例。通过学习本文,你将掌握如何有效地使用 Jest 提高代码质量和稳定性。
30 1
|
1月前
|
测试技术
产品测试
【10月更文挑战第10天】产品测试
13 2
|
17天前
|
资源调度 前端开发 JavaScript
React 测试库 React Testing Library
【10月更文挑战第22天】本文介绍了 React Testing Library 的基本概念和使用方法,包括安装、基本用法、常见问题及解决方法。通过代码案例详细解释了如何测试 React 组件,帮助开发者提高应用质量和稳定性。
28 0
|
1月前
|
前端开发 JavaScript 应用服务中间件
linux安装nginx和前端部署vue项目(实际测试react项目也可以)
本文是一篇详细的教程,介绍了如何在Linux系统上安装和配置nginx,以及如何将打包好的前端项目(如Vue或React)上传和部署到服务器上,包括了常见的错误处理方法。
265 0
linux安装nginx和前端部署vue项目(实际测试react项目也可以)
|
1月前
|
监控 测试技术 数据安全/隐私保护
新产品测试流程如何?
新产品测试流程如何?【10月更文挑战第10天】
89 0
|
2月前
|
测试技术 持续交付 UED
软件测试的艺术与科学:平衡创新与质量的探索在软件开发的波澜壮阔中,软件测试如同灯塔,指引着产品质量的方向。本文旨在深入探讨软件测试的核心价值,通过分析其在现代软件工程中的应用,揭示其背后的艺术性与科学性,并探讨如何在追求技术创新的同时确保产品的高质量标准。
软件测试不仅仅是技术活动,它融合了创造力和方法论,是软件开发过程中不可或缺的一环。本文首先概述了软件测试的重要性及其在项目生命周期中的角色,随后详细讨论了测试用例设计的创新方法、自动化测试的策略与挑战,以及如何通过持续集成/持续部署(CI/CD)流程优化产品质量。最后,文章强调了团队间沟通在确保测试有效性中的关键作用,并通过案例分析展示了这些原则在实践中的应用。
71 1
|
2月前
|
测试技术 UED 开发者
软件测试的艺术:从代码审查到用户反馈的全景探索在软件开发的宇宙中,测试是那颗确保星系正常运转的暗物质。它或许不总是站在聚光灯下,但无疑是支撑整个系统稳定性与可靠性的基石。《软件测试的艺术:从代码审查到用户反馈的全景探索》一文,旨在揭开软件测试这一神秘面纱,通过深入浅出的方式,引领读者穿梭于测试的各个环节,从细微处着眼,至宏观视角俯瞰,全方位解析如何打造无懈可击的软件产品。
本文以“软件测试的艺术”为核心,创新性地将技术深度与通俗易懂的语言风格相结合,绘制了一幅从代码审查到用户反馈全过程的测试蓝图。不同于常规摘要的枯燥概述,这里更像是一段旅程的预告片,承诺带领读者经历一场从微观世界到宏观视野的探索之旅,揭示每一个测试环节背后的哲学与实践智慧,让即便是非专业人士也能领略到软件测试的魅力所在,并从中获取实用的启示。