前端单元测试简介

2025-06-04

前端单元测试简介

好吧,您已经涉足了 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)
  })
})


Enter fullscreen mode Exit fullscreen mode

让我们分解一下代码:

  1. 我们要测试的方法。正如我们之前提到的,单元测试通常适用于方法或 UI 元素交互。了解需要测试什么的好方法是从头开始查看应用程序的各个组件。“我的方法接受什么输入?它的输出是什么?”,“我的方法会影响组件的状态吗?”,“有哪些边缘情况?”这些都是很好的起点。
  2. 测试套件,应进行简要描述,并将相关的单元测试分组。例如,一个测试套件可以包含与特定方法相关的所有测试。您可以根据需要声明任意数量的测试套件,其主要作用是提高测试日志的可读性。
  3. 单元测试,附有描述,回调内的语句就是测试本身。
  4. 测试断言。测试的核心在于断言,将给定值与预期值进行比较。这里,我们add以 1 和 1 作为参数,给出方法的返回值,并期望结果为 2。

我们可以添加的其他测试

以下是针对此示例合理添加的一些其他测试:

检测阴性结果:



it("should return -2", () => {
  expect(add(0, -2)).toBe(-2)
})


Enter fullscreen mode Exit fullscreen mode

测试我们方法的错误处理(当传递数字以外的任何内容作为参数时):



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


Enter fullscreen mode Exit fullscreen mode

注意:你可能注意到我们使用了toThrow()而不是toBe()。Jest 提供了大量的匹配器来检查某个值是否与给定的结果匹配。因此,你可以检查某个值是否是nulltrue、大于或小于 等等。

单元测试的具体示例🧪

好吧,我已经展示了一个非常不切实际的单元测试示例,所以让我们从头到尾在真实的组件上尝试一下。

我使用 创建了一个项目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() });


Enter fullscreen mode Exit fullscreen mode

注意:如果您使用的是旧版本的 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);
});


Enter fullscreen mode Exit fullscreen mode

所有测试文件的格式都类似:*.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.


Enter fullscreen mode Exit fullscreen mode

太棒了,我们已经运行了第一个单元测试。

我们现在将构建一个基本的计数器应用,让用户点击按钮来增加屏幕上的数值。源代码可以在这里找到,欢迎克隆或 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>
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

那么我们从哪里开始呢?问问自己,我们可以测试组件的哪些方面,在本例中:

  • 屏幕上最初显示的内容
  • 当用户点击按钮时,计数器会增加

测试渲染的值

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


Enter fullscreen mode Exit fullscreen mode

您可能注意到了一些事情,所以让我们来看看代码。

  1. 正如我们前面提到的,Jest 允许我们创建测试套件来组织我们的测试。
  2. 有时,您希望在测试运行之前设置某些内容,或在测试运行之后包装其他内容。因此,Jest 提供了 setup 和 teardown 辅助函数,您可以在此处阅读。您会发现自己最常用的函数是 和 ,beforeEach因为beforeAll它们允许您渲染组件,这就引出了第三点。
  3. 浅渲染是 Enzyme 提供的少数几种渲染方法之一。在浅渲染的情况下,我们会渲染组件本身,而不渲染其子组件。这允许您将组件作为一个单元进行测试,这样,如果您修改子组件,就不会影响当前正在测试的组件。您可以看到 Enzyme 的渲染方式是组件的一个实例,就像组件首次出现在屏幕上时一样,包含其内部状态、HTML 等所有内容。
  4. 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")
})


Enter fullscreen mode Exit fullscreen mode

我们使用包装器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
PREV
鲜为人知的 npm CLI 命令
NEXT
弹性盒子 101