前端单元测试简介
好吧,您已经涉足了 Web 开发,做了一些项目,部署了一些应用程序,这些帮助您掌握了前端开发的基本概念,从路由、服务器端渲染、状态管理到异步查询等。
但有一件事你还没付诸实践,要么是因为你刻意回避它,要么仅仅是因为你没意识到它的用处:测试。更具体地说,是前端单元测试,它是测试生态系统中非常重要的一个组成部分。
我今天早些时候在我的博客上发布了这篇文章,希望得到大家的反馈,尽情享受吧!
单元测试?🧐
我将简单介绍一下单元测试在日常应用中的基础知识。
本质上,前端代码测试可以分为三类:
E2E 测试,即端到端测试,是指测试应用程序的执行是否从始至终都符合设计要求。整个应用程序在真实场景中进行测试,包括测试数据库、网络、API 等组件之间的通信,以及在各种浏览器中执行代码。基本上,测试所有内容。设置过程需要大量时间,成本也最高。
集成测试包括测试应用程序元素之间的交互,例如 UI 和 API 之间的通信。它设置时间较短,而且成本较低。
单元测试则有所不同,因为它将代码中独立的部分作为单元进行测试。这些单元通常以方法、属性、UI 元素操作等形式出现。单元测试的实施速度最快,成本也最低。
你可能已经注意到,在我们的金字塔中,你爬得越高,设置测试所需的时间和金钱就越多。这就是为什么很多项目倾向于关注单元测试,因为它们可以覆盖大多数场景,帮助你了解代码是否真正有效,节省时间,并简化部署流程。
单元测试示例⚙️
在我们深入探讨之前,值得一提的是,什么是测试框架。
测试框架可以让你轻松搭建测试环境并运行测试套件。你可以将测试框架视为 UI 开发中的 React 或 Vue,它们提供丰富的工具,让你的工作更轻松。
我强烈推荐Jest,因为它在大多数项目中都很常见,并且由 Facebook 一支优秀的工程师团队维护。请注意,我将在我的示例中使用这个框架。
我将介绍一些单元测试的基本示例,让我们开始吧。如果您想继续学习,可以使用一个名为TDDBin的网站。
// 1. The method we want to test
function add(x, y) {
return x + y
}
// 2. A test suite
describe("add method", () => {
// 3. A unit test
it("should return 2", () => {
// 4. An assertion
expect(add(1, 1)).toBe(2)
})
})
让我们分解一下代码:
- 我们要测试的方法。正如我们之前提到的,单元测试通常适用于方法或 UI 元素交互。了解需要测试什么的好方法是从头开始查看应用程序的各个组件。“我的方法接受什么输入?它的输出是什么?”,“我的方法会影响组件的状态吗?”,“有哪些边缘情况?”这些都是很好的起点。
- 测试套件,应进行简要描述,并将相关的单元测试分组。例如,一个测试套件可以包含与特定方法相关的所有测试。您可以根据需要声明任意数量的测试套件,其主要作用是提高测试日志的可读性。
- 单元测试,附有描述,回调内的语句就是测试本身。
- 测试断言。测试的核心在于断言,将给定值与预期值进行比较。这里,我们
add
以 1 和 1 作为参数,给出方法的返回值,并期望结果为 2。
我们可以添加的其他测试
以下是针对此示例合理添加的一些其他测试:
检测阴性结果:
it("should return -2", () => {
expect(add(0, -2)).toBe(-2)
})
测试我们方法的错误处理(当传递数字以外的任何内容作为参数时):
function add(x, y) {
// Check if the parameters are numbers
// If not, throw an error
if (isNaN(x) || isNaN(y)) {
throw new Error("Parameter is not a number !")
}
return x + y
}
describe("add method", () => {
it("should throw an error if NaN is given as parameter", () => {
expect(add).toThrow()
})
})
注意:你可能注意到我们使用了
toThrow()
而不是toBe()
。Jest 提供了大量的匹配器来检查某个值是否与给定的结果匹配。因此,你可以检查某个值是否是null
、true
、大于或小于 等等。
单元测试的具体示例🧪
好吧,我已经展示了一个非常不切实际的单元测试示例,所以让我们从头到尾在真实的组件上尝试一下。
我使用 创建了一个项目create-react-app
,它开箱即用,并且已设置好 Jest。无论您使用哪种框架,大多数 CLI 都会为您配置 Jest,因此您只需创建测试文件并编写测试即可!如果您不使用其中任何一种 CLI,或者只需要从头开始配置 Jest,请随时阅读其入门文档。
现在,让我们安装Enzyme,它允许我们通过渲染组件来测试其输出。需要注意的是,有很多知名的工具可以用来测试前端应用程序,其中最著名的是 Jest 和 Enzyme。
让我们按照他们的介绍文档安装必要的软件包:
npm i --save-dev enzyme enzyme-adapter-react-16 react-test-renderer
然后我们需要通过创建以下文件来设置我们的适配器:
// /src/setupTests.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
注意:如果您使用的是旧版本的 React,请确保为您使用的版本配置正确的适配器,请随时阅读其安装文档。
您可能已经注意到create-react-app
创建了以下单元测试:
// App.spec.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
所有测试文件的格式都类似:*.spec.js
或者*.test.js
取决于你的偏好。我个人总是使用第一种格式。😄
npm run test
在控制台中运行尝试一下。你应该得到以下输出:
PASS src/App.spec.js
✓ renders without crashing (2ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.097s, estimated 1s
Ran all test suites.
太棒了,我们已经运行了第一个单元测试。
我们现在将构建一个基本的计数器应用,让用户点击按钮来增加屏幕上的数值。源代码可以在这里找到,欢迎克隆或 fork 代码库来试用。
我们的组件如下所示:
class App extends Component {
state = {
counter: 0
}
handleClick = () => {
this.setState(state => {
return {
counter: state.counter + 1
}
})
}
render() {
return (
<div className="App">
<header className="App-header">
<h1>{this.state.counter}</h1>
<button className="button" onClick={this.handleClick}>
Click Me !
</button>
</header>
</div>
);
}
}
那么我们从哪里开始呢?问问自己,我们可以测试组件的哪些方面,在本例中:
- 屏幕上最初显示的内容
- 当用户点击按钮时,计数器会增加
测试渲染的值
Enzyme 的类似 JQuery 的语法和 Jest 的断言使得测试这些情况变得非常容易,以下是我们应该如何进行:
import React from 'react';
import App from './App';
import { shallow } from 'enzyme'
// 1. Test suite
describe("[UNIT] Testing the App component", () => {
let wrapper
// 2. A Jest setup helper function
beforeEach(() => {
// 3. Enzyme's shallow rendering
wrapper = shallow(<App/>)
})
describe("Component validation", () => {
// 4. Our unit test, checking if the initial value is 0
it('displays 0 as a default value', () => {
expect(wrapper.find("h1").text()).toContain("0")
})
})
})
您可能注意到了一些事情,所以让我们来看看代码。
- 正如我们前面提到的,Jest 允许我们创建测试套件来组织我们的测试。
- 有时,您希望在测试运行之前设置某些内容,或在测试运行之后包装其他内容。因此,Jest 提供了 setup 和 teardown 辅助函数,您可以在此处阅读。您会发现自己最常用的函数是 和 ,
beforeEach
因为beforeAll
它们允许您渲染组件,这就引出了第三点。 - 浅渲染是 Enzyme 提供的少数几种渲染方法之一。在浅渲染的情况下,我们会渲染组件本身,而不渲染其子组件。这允许您将组件作为一个单元进行测试,这样,如果您修改子组件,就不会影响当前正在测试的组件。您可以看到 Enzyme 的渲染方式是组件的一个实例,就像组件首次出现在屏幕上时一样,包含其内部状态、HTML 等所有内容。
h1
我们的第一个测试很简单:我们通过向方法传递选择器来查找组件的标题find
并直接访问其文本;然后我们使用 Jest 的断言方法检查它是否包含值 0。很简单,对吧?
好的,开始我们的第二个测试。
测试事件
借助 Enzyme,测试事件变得非常简单,下面是我们如何测试单击按钮是否会增加计数器:
it("should increase counter when the button is clicked", () => {
wrapper.find("button").simulate("click")
expect(wrapper.find("h1").text()).toContain("1")
})
我们使用包装器simulate
上的方法button
来触发事件,然后检查我们的标题以查看它是否等于 1。
注意:大多数事件类型可以使用模拟方法模拟,包括输入、点击、焦点、模糊、滚动等。
测试代码覆盖率
要掌握的一个重要概念是代码覆盖率,它表示被测试代码的百分比。
代码覆盖工具检查以下内容:
- 语句:您的代码执行了多少条语句。
- 分支:由条件语句(if/else)创建的分支,可能会执行,也可能不会执行。
- 函数:已调用的函数数量。
- 行数:测试期间执行的行数比例。
看起来像这样(基于我们之前的例子):
我们最常用的代码覆盖率工具之一叫做Istanbul,当您运行以下命令时,create-react-app 会使用它来报告应用程序的代码覆盖率npm run test --coverage
。
像 Istanbul 这样的工具会以 HTML 文件的形式生成代码覆盖率报告,帮助您概览代码中哪些部分尚未测试。它会突出显示单元测试中未覆盖的特定行,以帮助您达到 100% 的覆盖率。
注意:代码覆盖率并非万能,100% 覆盖率并不意味着您已经测试了给定组件的所有场景,因此您只应在合理的情况下努力实现这一目标。正如@edaqa
所指出的,代码覆盖率可能被视为一个糟糕的指标,因为它可能“通过将执行的行数与测试的行数等同起来,从而提供一种虚假的安全感”,以及其他一些问题。因此,请谨慎使用它来概览代码的覆盖范围,并且不要将指标与目标混淆。
荣誉提名👏
以下是一些我没有谈到但值得提及的事情(无特定顺序):
- Jest 有一个
--watch
选项,允许在测试文件发生更改时自动运行测试。 - 一份很棒的酶小抄。
- 确保检查代码覆盖率报告期间生成的文件,准确了解哪些行尚未覆盖可以节省大量时间。
- 确保您正在测试需要测试的内容,避免测试第三方包是否完成其工作,而专注于测试您的组件是否符合您的规格。
- 测试驱动开发 (TDD) 的概念可以描述如下:“测试驱动开发是指首先确定程序的功能(规范),然后制定一个失败测试,最后编写代码使测试通过”(链接)。如果您暂时无法完全理解,也不用担心,但最终理解它的价值并知道将来可能会被要求实践它,这一点很重要。这是一个很好的起点。
总结
我相信这些信息对于前端测试的介绍来说已经足够了,并且可以帮助您学习有关单元测试的很多知识。
现在你可能觉得测试既费时又没用,但相信我,你最终会意识到测试应用程序的重要性。它将帮助你调试和构建代码,节省时间,减少技术负担,改进工作流程,并从长远来看全面提升你的生产力。
与往常一样,非常感谢您花时间阅读本文,我希望您能有所收获。
如果您有任何问题,请随时通过 Twitter @christo_kade发送给我,如果您喜欢这篇文章,请关注我,这样当我上传任何新内容时,您就会收到通知!
文章来源:https://dev.to/christopherkade/introduction-to-front-end-unit-testing-510n