前端单元测试入门与最佳实践(2)

发表于:2022-1-12 09:44

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:字节架构前端    来源:稀土掘金

分享:
  Testing Library
  核心功能
  ·强大的 Query 能力
    提供丰富的 API 使得用户可以轻松的在 JS-DOM 中查询、获取元素;
  · 模拟触发用户操作触发的事件
    提供了基础的 fireEvent 能力;
    也可以使用官方提供的更强大的 user-event。
  前端框架友好
  在核心功能之上,Testing Library 还支持 React、Vue、Angular、Svelte、Cypress 等框架,进一步的降低接入单元测试的成本。
  除此之外,还提供了 @testing-library/jest-dom 工具,在 jest 的运行环境下引入该工具,可以为你的断言提供更多的判断方法,比如:
  toBeDisabled
  toBeEnabled
  toBeEmptyDOMElement
  toBeInTheDocument
  toBeInvalid
  toBeRequired
  toBeValid
  toBeVisible
  toContainElement
  toContainHTML
  toHaveAccessibleDescription
  toHaveAccessibleName
  toHaveAttribute
  toHaveClass
  toHaveFocus
  toHaveFormValues
  toHaveStyle
  toHaveTextContent
  toHaveValue
  toHaveDisplayValue
  toBeChecked
  toBePartiallyChecked
  toHaveErrorMessage
  React Testing Library vs Enzyme
  在此之前大部分的 React 项目会选择基于 Enzyme 来作为运行时,在 React 官方推荐 Testing Library 后越来越多的新项目开始迁移过来。两者的测试用例哲学有着明显的差异,Enzyme 提供给开发者访问 React Component state 的能力,你常常能够在测试用例中看到对组件状态的断言。
  待测试源码:
  import React from "react";
  class Counter extends React.Component {
    constructor() {
      this.state = {
        count: 0,
      };
    }
    
    increment = () => {
      this.setState(({ count }) => ({ count: count + 1 }));
    }
    
    decrement = () => {
      this.setState(({ count }) => ({ count: count - 1 }));
    }
    
    render() {
      return (
        <div>
          <button onClick={this.decrement}>-</button>
          <p>{this.state.count}</p>
          <button onClick={this.increment}>+</button>
        </div>
      );
    }
  }
  export default Counter;

  Enzyme 测试用例:
  import React from "react";
  import { shallow } from "enzyme";
  import Counter from "./counter";
  describe("<Counter />", () => {
    test("properly increments and decrements the counter", () => {
      const wrapper = shallow(<Counter />);
      // 对组装内部状态进行断言
      expect(wrapper.state("count")).toBe(0);
      // 触发组件实例上的方法
      wrapper.instance().increment();
      expect(wrapper.state("count")).toBe(1);
      wrapper.instance().decrement();
      expect(wrapper.state("count")).toBe(0);
    });
  });
  
  React Testing Library 测试用例:
  import React from "react";
  import { render, screen, fireEvent } from "@testing-library/react";
  import Counter from "./counter";
  describe("<Counter />", () => {
    it("properly increments and decrements the counter", () => {
      render(<Counter />);
      // 通过 getByText 或者节点,也可以通过无障碍属性获取
      const counter = screen.getByText("0");
      const incrementButton = screen.getByText("+");
      const decrementButton = screen.getByText("-");
      // 触发用户操作
      fireEvent.click(incrementButton);
      expect(counter.textContent).toEqual("1");
      fireEvent.click(decrementButton);
      expect(counter.textContent).toEqual("0");
    });
  });

  从上面两个示例中可以明显看出两种框架的测试哲学不同之处,Enzyme 更偏向于对代码进行控制来完成测试流程,React Testing Library 更倾向于事件驱动,通过模拟用户操作来完成测试流程,现在越来越多的测试框架开始向后者靠近。
  推荐使用
  配置参数太多不想看怎么办?
  jest 提供了类似 npm init 的能力,执行下方命令会为你创建最小可用的配置文件:
  jest --init

  如何组织测试用例文件?
  过去推荐
  在早期组织单元测试用例时,通常建议在 src 目录下创建 __tests__ 目录,按照源码的目录结构组织测试代码,部分人会在这个目录结构之上再增加一层目录用以区分 unit 和 integration。
  现在推荐
  越来越多的语言自带的测试框架(Go),还有新测试框架开始推荐将测试用例放在你的源码边(也有人称之为领域驱动管理),这个方式主要带来的好处有:
  ·能够快速的找到你的测试用例和源码;
  ·减少许多无意义的文件夹嵌套;
  ·当一个模块废弃的时候,我们可以快速的移除所有与他相关的文件。
  如何配置通用环境?
  在 Jest 中提供了 setupFilesAfterEnv 配置,可以指向我们的脚本文件,该脚本的内容会在测试框架环境运行之后,执行测试用例之前执行:
  import "@testing-library/jest-dom"; // 扩展你的断言方法
  import routeData from "react-router";
  // mock 全局 i8n 方法,放弃网络请求,直接使用降级处理
  window.i18n = (key, options, fallback) => fallback;
  const mockLocation = {
    pathname: "/",
    hash: "",
    search: "?test=initial",
    state: "",
  };
  const mockHistory = {
    replace: ({ search }) => {
      mockLocation.search = search;
    },
  };
  // mock react router hooks
  beforeEach(() => {
    jest.spyOn(routeData, "useLocation").mockReturnValue(mockLocation);
    jest.spyOn(routeData, "useHistory").mockReturnValue(mockHistory);
  });

  如何 polyfill 浏览器的方法和属性?
  代码中肯定会使用到 BOM / DOM 方法,这类方法通过直接 window.func = () => { ... } 是无法直接覆写的,此时可以使用 defineProperty 重写。这类 polyfill 建议统一放到 polyfill 目录下,在 setupFilesAfterEnv 时全部引入即可。
  // test-utils/polyfill/matchMedia.js
  Object.defineProperty(window, "matchMedia", {
    writable: true,
    value: jest.fn().mockImplementation(query => ({
      matches: false,
      media: query,
      onchange: null,
      addListener: jest.fn(), // Deprecated
      removeListener: jest.fn(), // Deprecated
      addEventListener: jest.fn(),
      removeEventListener: jest.fn(),
      dispatchEvent: jest.fn(),
    })),
  });

  如何 mock 第三方 node_modules?
  在 node_modules 同级目录下创建文件夹 __mocks__,然后创建和 npm package name 同名的 JavaScript 文件即可,例如:
  - node_modules
  - __mocks__
    - react.js
    - lodash.js
  - src
    - components
    - App.jsx
    - main.js

  现在你的代码中对于 react 和 lodash 的引用全都会指向 mocks 目录下的 react.js 和 lodash.js。
  如果依赖的第三方包具有 scope,则需要增加一个目录,命名为 scope 即可,例如:
  - node_modules
  - __mocks__
    - @arco-design
      - web-react.js
  - src
    - components
    - App.jsx
    - main.js

  单测覆盖率怎么看?
  Statements(语句覆盖率):是否每个语句都执行了。
  Branches(分支覆盖率):是否每个判断都执行了。
  Functions(函数覆盖率): 是否每个函数都执行了。
  Lines(行覆盖率):是否每行都执行了,大部分情况下等于 Statements。
  如何将单测接入到研发流程中?
  对一个现存项目进行单元测试接入是比较困难的,我们可以结合 Gitlab 的能力,在 Merge Request 的环节对增量代码进行单测覆盖率的准入红线限制。如下图,我们对 MR 的单测覆盖率、reviewer、title、work item 绑定均进行了检查,增量代码必须满足至少 15% 的覆盖率红线才能够达到 approve 标准。

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
价值398元的测试课程免费赠送,填问卷领取吧!

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计

法律顾问:上海漕溪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2022
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号