前端自动化测试概述

0x01 概述

  • 在敏捷迭代开发中,测试能够及时暴露问题并修正,从而降低需求不匹配风险,更快体现产品价值
  • 测试能够提高开发效率,降低代码复杂度,使代码易于重构,结构也更加完善
    • 重构:在不改变软件可观测行为的前提下,改善代码内部设计或实现
  • 测试类型:(成本逐渐升高、速度逐渐变慢、距离用户更近)
    1. 单元测试(unit test)
    2. 集成测试(integration test)
    3. 端到端测试(e2e test)
  • 实际意义:
    • 安全重构已有代码
    • 快速回归已有功能
    • 保存业务上下文

0x02 基础单元测试

(1)概述

  • 官网:https://jestjs.io/zh-Hans/
  • Jest 由 Meta(原 Facebook)开发,现由 OpenJS 管理
  • 特点:快速、开箱即用、守护模式、快照测试
  • 通过命令 jest 启用,常用选项:
    • --watch:实时监听测试变化
    • --coverage:查看测试覆盖度

(2)基础使用

  1. 使用命令 npm init -y 快速创建一个 NodeJS 环境

  2. 使用命令 npm install --save-dev jest 安装 Jest 到开发依赖

  3. 修改 package.json

    "scripts": {
      "test": "jest"
    },
    
  4. 创建目录 src 用于存放代码文件

  5. 创建并编写以下文件:

    1. src\calc.js

      module.exports = {
        add: (a, b) => {
          return a + b;
        },
      };
      
      
    2. src\calc.test.js

      /**
       * 测试计算模块的核心功能
       *
       * 该测试套件主要验证计算模块中基础算术功能的正确性
       */
      const { add } = require("./calc");
      
      // 主测试套件:涵盖计算模块的基础运算验证
      describe("计算模块", () => {
        // 测试用例:验证加法运算的基础功能
        // 预期行为:正确执行两个数字的加法运算并返回合法结果
        test("计算1+2", () => {
          // 验证加法函数对整数参数的处理
      
          // Given
          const number = 1;
          const anotherNumber = 2;
      
          // When
          const result = add(number, anotherNumber);
      
          // Then
          expect(result).toBe(3);
        });
      });
      
      
      说明
      Given Arrange 准备测试数据
      可以抽取到 beforeEach 或 mock 模块
      When Act 采取行动
      调用相应的模块,执行对应的函数或组件渲染方法
      Then Assert 断言
      借助 Matchers 的能力,Jest 可以扩展自定义 Matcher
  6. 使用命令 npm run test 执行测试

(3)常见 Matcher

  • .toBe():严格相等,即 ===

  • .toEqual():相等,即 ==

    const obj1 = {
      name: "obj",
    };
    const obj2 = {
      name: "obj",
    };
    
    describe("等于判断", () => {
      test("toBe", () => {
        expect(obj1).toBe(obj2); // failed
      });
      test("toEqual", () => {
        expect(obj1).toEqual(obj2); // passed
      });
    });
    
    
  • .toBeFalsy():判断布尔值是否为 false

    describe("布尔值判断", () => {
      test("true", () => {
        expect(true).toBeFalsy(); // failed
      });
      test("false", () => {
        expect(false).toBeFalsy(); // passed
      });
    });
    
    
  • .toHaveLength():判断数组长度

    test("长度是否为3", () => {
      expect([1, 2, 3]).toHaveLength(3);
    });
    
    
  • .toHaveBeenCalled():判断当前方法是否被调用

  • .toHaveBeenCalledTimes():判断当前方法被调用次数

    const mockFn = jest.fn();
    
    describe("调用判断", () => {
      test("是否被调用", () => {
        expect(mockFn).toHaveBeenCalled(); // failed
        mockFn(1, 2, 3);
        expect(mockFn).toHaveBeenCalled(); // passed
      });
      test("调用次数", () => {
        mockFn(1, 2, 3);
        expect(mockFn).toHaveBeenCalledTimes(1); // passed
        expect(mockFn).toHaveBeenCalledTimes(2); // failed
      });
    });
    
    
  • .toThrow():判断抛出的异常

    const myError = new Error("myError");
    
    test("异常为 myError", () => {
      mockFn.mockImplementation(() => {
        throw myError;
      });
      expect(mockFn).toThrow(myError);
    });
    
    
  • .toMatchSnapshot():捕获组件或对象的输出,并将其存储为快照文件,判断此次快照与上次快照是否匹配

  • .toMatchInlineSnapshot():捕获组件或对象的输出,并将其内联存储到测试文件,判断此次快照与上次快照是否匹配

    describe("快照判断", () => {
      test("默认", () => {
        expect(1 + 1).toMatchSnapshot();
      });
      test("内联", () => {
        expect(1 + 1).toMatchInlineSnapshot(`2`);
      });
    });
    
    
  • .extend():扩展自定义 Matcher

    test("加一的结果是否大于 0", () => {
      expect.extend({
        toBeGreaterThanZero(received) {
          const pass = received + 1 > 0;
          if (pass) {
            return {
              message: () => `expected ${received} not to be greater than 0`,
              pass: true,
            };
          }
          return {
            message: () => `expected ${received} to be greater than 0`,
            pass: false,
          };
        },
      });
      expect(0).toBeGreaterThanZero(); // passed
      expect(-1).toBeGreaterThanZero(); // failed
    });
    
    

(4)模块间依赖

  • 测试单元分类:
    • 社交型:需要依赖其他模块来实现功能的测试模块
    • 独立型:无需依赖其他模块来实现功能的测试模块
  • Mock:用于实现代替外部模块
    Stub:用于模拟特定行为
    Spy:用于监听模块行为
  • 易测性:模块职责越单一,单元测试越方便,体现可维护性

Mock

  1. src\service.js

    module.exports = {
      getNames() {
        return ["Alex", "Bob", "Charles"];
      },
    };
    
  2. src\app.js

    const service = require("./service");
    
    module.exports = {
      searchNames: (term) => {
        const matches = service.getNames().filter((name) => name.includes(term));
        return matches.length > 3 ? matches.slice(0, 3) : matches;
      },
    };
    
  3. src\app.test.js

    const { searchNames } = require("./app");
    
    test("empty", () => {
      const keyword = "David";
      const result = searchNames(keyword);
      expect(result).toEqual([]);
    });
    
    test("Alex and Charles", () => {
      const keyword = "e";
      const result = searchNames(keyword);
      expect(result).toEqual(["Alex", "Charles"]);
    });
    
  4. 引入 Mock 替代 Service 模块

    const { searchNames } = require("./app");
    
    jest.mock("./service", () => ({
      getNames: jest.fn(() => ["David"]),
    }));
    
    test("David", () => {
      const keyword = "David";
      const result = searchNames(keyword);
      expect(result).toEqual(["David"]);
    });
    
    test("empty", () => {
      const keyword = "Alex";
      const result = searchNames(keyword);
      expect(result).toEqual([]);
    });
    
  5. 分情况实现 Mock

    const { searchNames } = require("./app");
    const { getNames } = require("./service");
    
    jest.mock("./service", () => ({
      getNames: jest.fn(),
    }));
    
    test("David", () => {
      const keyword = "David";
      getNames.mockImplementation(() => ["David"]);
      const result = searchNames(keyword);
      expect(result).toEqual(["David"]);
    });
    
    test("empty", () => {
      const keyword = "Alex";
      getNames.mockImplementation(() => ["Alice"]);
      const result = searchNames(keyword);
      expect(result).toEqual([]);
    });
    
  6. 异常情况 Mock

    const { searchNames } = require("./app");
    const { getNames } = require("./service");
    
    jest.mock("./service", () => ({
      getNames: jest.fn(),
    }));
    
    test("undefined and null", () => {
      getNames.mockImplementation(() => []);
      expect(searchNames(undefined)).toEqual([]);
      expect(searchNames(null)).toEqual([]);
    });
    
    test("UPPERCASE", () => {
      const keyword = "alex";
      getNames.mockImplementation(() => ["Alex"]);
      const result = searchNames(keyword);
      expect(result).toEqual([]);
    });
    

快照测试

基于上述 Mock 中的文件

  1. src\app.test.js

    const { searchNames } = require("./app");
    
    test("undefined and null", () => {
      expect(searchNames("David")).toMatchInlineSnapshot();
    });
    
  2. 运行测试后,.toMatchInlineSnapshot() 的参数值会自动替换成相应的结果

    const { searchNames } = require("./app");
    
    test("undefined and null", () => {
      expect(searchNames("David")).toMatchInlineSnapshot(`[]`);
    });
    

.toMatchSnapshot() 运行后会生成一个文件,相比 .toMatchInlineSnapshot() 可读性较差

0x03 UI 单元测试

  • 前端组件化极大地方便了对 UI 的单元测试

(1)Testing Library

  • 官网:https://testing-library.com

  • Testing Library 可用于实现 UI 单元测试,适用于当前主流前端框架,如 Vue、React 等

  • 查询组件渲染元素

    方法 无结果 唯一结果 多个结果 是否异步
    getBy 报错 返回 报错
    findBy 报错 返回 报错
    queryBy 返回 null 返回 报错
    getAllBy 报错 数组 数组
    findAllBy 报错 数组 数组
    queryAllBy 空数组 [] 数组 数组
  • 交互行为测试:click()dblClick()keyboard()hover()paste()

  • 举例:React + Testing Library

    1. 使用命令 npx create-react-app react-test 创建名为 react-test 的 React 项目

      • 此时项目中自带 Testing Library
    2. 移除不必要的文件,修改以下文件

      App.js

      import { useState } from "react";
      
      function App() {
        const [name, setName] = useState("");
      
        return (
          <div className="App">
            <input
              type="text"
              placeholder="Type your name"
              value={name}
              onChange={(e) => setName(e.target.value)}
            />
            <p>
              My name is <span>{name}</span>
            </p>
          </div>
        );
      }
      
      export default App;
      

      App.test.js

      import { render, screen } from "@testing-library/react";
      import userEvent from "@testing-library/user-event";
      import App from "./App";
      
      test("my name", () => {
        render(<App />);
        const myName = "SRIGT";
      
        const element = screen.getByPlaceholderText("Type your name");
        userEvent.type(element, myName);
      
        expect(screen.getByText(myName)).toBeInTheDocument();
      });
      
    3. 使用命令 npm run test 执行测试

(2)异步测试(MSW)

  • 官网:https://mswjs.io/

  • MSW(Mock Service Worker)能够在 Service Worker 一级进行拦截并模拟返回的结果

  • 以 API 网络异步请求为例:

    1. 使用命令 npm install --save-dev msw@1 安装 MSW

    2. 修改 App.js

      import { useEffect, useState } from "react";
      
      function App() {
        const [name, setName] = useState("");
      
        useEffect(async () => {
          const response = await fetch("https://randomuser.me/api/?inc=name", {
            method: "GET",
          });
          const reply = await response.json();
          const { first, last } = reply.results[0].name;
          setName(`${first} ${last}`);
        }, []);
      
        return (
          <div className="App">
            <input
              type="text"
              placeholder="Type your name"
              value={name}
              onChange={(e) => setName(e.target.value)}
            />
            <p>
              My name is <span>{name}</span>
            </p>
          </div>
        );
      }
      
      export default App;
      
    3. 新建 src\mocks\handlers.js

      import { rest } from "msw";
      
      export const handlers = [
        rest.get("https://randomuser.me/api/", (request, response, context) => {
          request.url.searchParams = {
            inc: "name",
          };
          return response(
            context.status(200),
            context.json({
              results: [
                {
                  name: {
                    title: "Mr",
                    first: "Eli",
                    last: "Sanchez",
                  },
                },
              ],
            })
          );
        }),
      ];
      
    4. 新建 src\mocks\server.js

      import { setupServer } from "msw/node";
      import { handlers } from "./handlers";
      
      export const server = setupServer(...handlers);
      
    5. 修改 src\setupTests.js

      import "@testing-library/jest-dom";
      import { server } from "./mocks/server";
      
      beforeAll(() => server.listen()); // 开始所有前监听
      afterEach(() => server.resetHandlers()); // 完成每个后重置
      afterAll(() => server.close()); // 结束所有后关闭
      
    6. 修改 App.test.js

      import App from "./App";
      
      test("random name", async () => {
        render(<App />);
      
        const text = await screen.findByText(/Eli Sanchez/gi);
      
        expect(text).toBeInTheDocument();
      });
      

(3)组件级集成测试(Cypress)

-End-

posted @ 2025-08-21 16:05  SRIGT  阅读(20)  评论(0)    收藏  举报