前端单元测试基本概念和技术选型

·  阅读 131

背景

测试就是有一个期望值,一个实际值,根据两者是否一致来判断是否通过。除了常规的逻辑测试外,前端还要对ui组件进行测试,对后者的测试,不同团队可能有不同的实现方式。比如

  • 将组件逻辑剥离转化为普通的 js 代码测试(不直接测试 UI,这种方式会对正常代码组织影响很大)
  • 以组件为单位测试具体的组件实例,比如调用实例方法和验证内部状态(比如Enzyme,由于过多关注实现细节,因此不方便维护)
  • 不关注具体实现,而是像用户使用一样测试对应功能(后文会介绍的React Testing Library)。

相关概念

如果对前端自动化测试相关概念比较熟悉,可以跳过这一节。

前端测试包括四个部分

在这个测试奖杯中,测试被分为四类,从下到上依次测试的范围依次增大。

  • Static 用来静态检查,比如eslinttypescript所处理的类型或语法错误,这部分一直在使用,基本保证了第三方库升级或小型重构后在解决完报错以后直接使用。

  • Unit 用来验证独立的代码片段能否正常工作

  • Integration 用来验证多个单元能否一起工作

  • End to End 用来像用户一样对应用程序操作,又叫做功能性测试

更具体的对比可以看这里

本节参考

具体的实践

回到具体的开发过程,前端的单元测试和集成测试并没有什么明显的界限,当我们测试一个组件没必要区分这是不是最小测试单元,甚至react 官方将 react 组件的测试分成了Rendering component treesRunning a complete app两类,前者测试组件树,这被归类到单元测试中,后者即为 e2e。

又由于 e2e 工作量比较大,最好和测试一同完成,因此本次只讨论单元测试。

技术选型

关于不同的 UI 测试工具前文有所涉及,这里不会做更多对比, 而是由于我们开发使用的是react,因此我们会按照其推荐(单元测试进行选型,即

其中 jest 是一个 js 测试框架(也被称为 test runner,用来执行测试脚本),React Testing Library 是使用在 jset 之上的一个测试 react 组件的 library。

jest

jest 提供了test 方法,或者用作it,用来写一个 test,可以用describe嵌套来组织不同的 test,比如

describe("test", () => {
  let a = 1;
  it("add1", () => {
    a++;
    expect(a).toEqual(2);
  });

  it("add1-again", () => {
    a++;
    expect(a).toEqual(3);
  });
});
复制代码

可以看到,在每个 test 中除了普通的 js 语句,还有expect方法用来验证被测试对象(expect 的参数)是否按期望(使用 expect 函数链式调用的方法来匹配)执行,每个 expect 方法被称为一个matcher

当在命令行执行jest时会执行以上测试脚本,并显示如下结果

  test
     add1 (2 ms)
     add1-again

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        5.922 s, estimated 6 s
复制代码

再比如我们像验证一个函数,可以将其引入验证

//math.js
function sum(x, y) {
  return x + y;
}
复制代码
import sum from "./math.js";

describe("sum", () => {
  test("sums up two values", () => {
    expect(sum(2, 4)).toBe(6);
  });
});
复制代码

除了以上的基本用法外,jest还提供了测试异步方法Mock 函数快照等。

React Testing Library

刚才看到的jest提供了对普通 js 逻辑的验证,我们项目中更多的是UI,即 react 组件。 @testing-library家族以@testing-library/dom为核心包,封装了各种场景的测试工具,React Testing Library(以下简称 RTL),即@testing-library/react就是其中之一,其封装了 react 官方测试工具react-dom/test-utils

RTL 的解决方案

当使用 RTL 进行测试时,是通过像用户使用一样,查找 dom 元素并与其交互的方式然后验证的方式来进行测试的,应避免测试实现细节,避免重构(修改实现但不修改功能)时造成测试用例失效。

比如

//counter.tsx
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  function increment() {
    setCount((c) => c + 1);
  }
  function decrement() {
    setCount((c) => c - 1);
  }
  return (
    <div>
      <button onClick={decrement}>-</button>
      <p>{count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

export default Counter;
复制代码
//counter.test.js
import React from "react";
import { render, fireEvent } from "@testing-library/react";

import Counter from "./counter";

describe("<Counter />", () => {
  it("properly increments and decrements the counter", () => {
    const { getByText } = render(<Counter />);
    const counter = getByText("0");
    const incrementButton = getByText("+");
    const decrementButton = getByText("-");

    fireEvent.click(incrementButton);
    expect(counter.textContent).toEqual("1");

    fireEvent.click(decrementButton);
    expect(counter.textContent).toEqual("0");
  });
});
复制代码

具体使用方法

使用 RTL 的测试,步骤如下,

  1. 相关基础设置,包括 jest 配置文件,测试前后的准备工作,比如 mock 服务器的启动和关闭。我们会把每个组件作为一个测试对象来处理,组件以外的依赖都通过 mock 实现,比如
  • 使用msw来 mock 对应 ajax 请求
  • 测试无关的静态资源比如 css 或图片
  • 运行环境缺失的 feature,比如window.matchMedia
  1. 实际测试之前应该渲染出组件,@testing-library/react提供了 render 方法。
  2. 渲染出组件后需要选择相关 dom 进行操作,这里我们使用上述 render 方法返回的方法,具体列表见这里
  3. 选择完 dom 后,需要对其进行操作,可以使用@testing-library/user-event模拟对应的事件,然后使用@testing-library/jest-dom提供的 matcher 进行验证。

比如

test("失败请求用例", async () => {
  server.use(
    rest.get("/api/v1/agent_management/brief_account_info", (req, res, ctx) => {
      return res(ctx.status(500));
    })
  );
  const { getByText, getByRole } = render(
    <FindingServer visible={true} onCancel={() => {}} />
  );
  userEvent.type(screen.getByTestId("input"), "13311112222");
  const searchBtn = getByText("查 询");
  fireEvent.click(searchBtn);
  await waitFor(() =>
    expect(getByRole(/search/)).not.toHaveClass("ant-btn-loading")
  );
  expect(screen.queryByText(/No Data/)).toBeInTheDocument();
});
复制代码
分类:
前端
收藏成功!
已添加到「」, 点击更改