怎么搞前端单元测试

什么是单元测试

维基百科对单元测试的定义:

计算机编程中,单元测试 (英语:Unit Testing)又称为模块测试 ,是针对程序模块软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

通俗的说就是对软件中的最小可测试单元进行检查和对这个单元的单个最终结果的某些假设进行校验。

测试金字塔

在金字塔模型之前,流行的是冰淇淋模型。包含了大量的手工测试、端到端的自动化测试及少量的单元测试。造成的后果是,随着产品壮大,手工回归测试时间越来越长,质量很难把控;这样的模式在测试效率、测试用例可维护性角度均存在一些问题,但又往往是很多团队自发进行测试自动化能力建设过程中的必经路径,问题积累到一定阶段需要逐步向测试金字塔方向演进。

测试金字塔是Mike Cohn在他的着作《敏捷的成功(Succeeding with Agile)》一书中提出了这个概念。这个比喻非常形象,它让你一眼就知道测试是需要分层的。它还告诉你每一层需要写多少测试。

测试金字塔中每层中涉及的测试技术均有自己的优势和局限性,由于上层UI测试的脆弱(不稳定性)、耗时(执行效率)问题,以及问题表现位置(UI)和问题根因位置(代码)距离太远的问题,测试金字塔由关注测试数量转向关注测试质量,推荐增加底层的测试投入。

  • 层次越靠上,开发复杂度越高,运行效率越低,交付进度受影响。
  • 端到端测试更容易遇到测试结果的不确定性。
  • 层次越靠下,单元隔离性越强,定位分析问题越容易。

为什么需要单元测试

根据金字塔原则,单元测试是需要增加投入的,但在现实中实践中听到很多声音是:

  • 单元测试浪费了太多的时间
  • 后面的测试同学会测出所有bug
  • 单元测试的成本效率不高我把测试都写了,那么测试人员做什么呢?
  • 公司请我来是写代码,而不是写测试

在《单元测试的艺术》这本书提到一个案例:找了开发能力相近的两个团队,同时开发相近的需求。进行单测的团队在编码阶段时长增长了一倍,从7天到14天,但是,这个团队在集成测试阶段的表现非常顺畅,bug量小,定位bug迅速等。最终的效果,整体交付时间和缺陷数,均是单测团队最少。

所以写好单元测试能给我们带来:

  • 正确性:测试可以验证代码的正确性,在上线前做到心里有底。
  • 自动化:当然手工也可以测试,通过console可以打印出内部信息,但是这是一次性的事情,下次测试还需要从头来过,效率不能得到保证。通过编写测试用例,可以做到一次编写,多次运行。
  • 解释性:测试用例是最好的文档之一,其他开发人员如果要使用组件API,那阅读测试用例是一种很好地途径,有时比文档说明更清晰。
  • 保证重构:互联网行业产品迭代速度很快,迭代后必然存在代码重构的过程,那怎么才能保证重构后代码的质量呢?有测试用例做后盾,就可以大胆的进行重构。

怎么写单元测试

要遵守的原则

在写单元测试的时候,基本上要遵守单元测试的原则:FIRST

  • F - 快速(Fast):测试过程一定要快,快速启动,快速验证,快速失败
  • I - 隔离(Isolate):测试用例间要相互隔离,无前后依赖关系
  • R - 可重复(Repeatable):测试用例是可重复执行的,在循环运行时,测试总能给出相同的结果
  • S - 自我验证(Self-validating):测试用例需要具备自我验证的能力,不要人工介入,当它们全部通过时,给出一个简单的"OK"报告,当它们失败时,描述简明的细节
  • T - 及时(Timely):测试是及时的,在代码上线前,及时地编写它们,以防止bug

单测需要测试什么

在遵守原则的基础上,写出的单元测试有一个RIGHT BICEP 法则:

  • Right - 结果正确:检查期望结果是否正确
  • B - 边界条件检查:是否所有的边界条件都是正确的
  • I - 反向关联检查:使用反向逻辑关系来验证
  • C - 交叉检查:使用其它手段交叉验证
  • E - 强制错误条件发生:强制引发错误,验证对异常处理情况
  • P - 满足性能需求:性能是否预期范围内

怎么衡量单测写的是否完善

单测完善程度的衡量用覆盖率来衡量。

前端测试框架

目前为止的web测试领域已经趋于稳定,逐渐成熟。截止到2022年,下面这张图从上至下就是目前为止业界使用最多的前端测试框架。在这其中每个框架有不同的特性,也有不同的定位。基本上包含着单元测试组件测试E2E测试,与大部分的测试体系一致。

单元测试框架

由于历史的发展,一些久远的单元测试库,例如AVA、Jasmine等就不参与此次单元测试库的比对,主要看一下上面图片提到的使用前二的单元测试库Jest和Mocha。

测试框架 断言 Mock 异步 快照
Jest 默认支持 默认支持 友好 默认支持
Mocha 不支持(可配置) 不支持(可配置) 友好 不支持(可配置)

jest满意度

Mocha满意度

根据上面数据显示,可见无论是满意度和配置上,Jest都有很大的优势,因此推荐你使用开箱即用的 Jest

Jest介绍

scss 复制代码
// main.js
function abs(a) {
  if (typeof a !== 'number') {
    throw new TypeError('参数必须为数值型')
  }

  if (a < 0) return -a
  return a
}

// test.spec.js
test('abs', () => {
  expect(abs(1)).toBe(1)
  expect(abs(0)).toBe(0)
  expect(abs(-1)).toBe(1)
  expect(() => abs('abc')).toThrow(TypeError) // 类型错误
})

expect和mathers

当我们写一个具体的测试时,需要两部分进行对比,即使用expect将待测试结果与表示期望结果的matchers进行匹配。最常用的expect语法是expect(value),后面添加.not表示取反。

其实matcher本身就是expect对象的一个方法。

  1. 常用的匹配器 最简单测试一个值的方法是使用精确匹配的方法。其中toBe使用 Object.is来进行精准匹配的测试。 如果您想要检查对象的值,请使用 toEqualtoStrictEqual 代替。
scss 复制代码
test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});
  1. 真值

代码中的undefined, null, and false有不同含义,若你在测试时不想区分他们,可以用真值判断。 Jest提供helpers供你使用。

  • toBeNull 只匹配 null
  • toBeUndefined 只匹配 undefined
  • toBeDefinedtoBeUndefined 相反
  • toBeTruthy 匹配任何 if 语句为真
  • toBeFalsy 匹配任何 if 语句为假
scss 复制代码
test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});

test('zero', () => {
  const z = 0;
  expect(z).not.toBeNull();
  expect(z).toBeDefined();
  expect(z).not.toBeUndefined();
  expect(z).not.toBeTruthy();
  expect(z).toBeFalsy();
});
  1. 数字

大多数的比较数字有等价的匹配器。

scss 复制代码
test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

对于比较浮点数相等,使用 toBeCloseTo 而不是 toEqual,因为你不希望测试取决于一个小小的舍入误差。

ini 复制代码
test('两个浮点数字相加', () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3);           这句会报错,因为浮点数有舍入误差
  expect(value).toBeCloseTo(0.3); // 这句可以运行
});
});
  1. 字符串

您可以检查对具有 toMatch 正则表达式的字符串︰

javascript 复制代码
test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});
  1. 数组和可迭代对象

你可以通过 toContain来检查一个数组或可迭代对象是否包含某个特定项:

scss 复制代码
const shoppingList = [  'diapers',  'kleenex',  'trash bags',  'paper towels',  'milk',];

test('shoppingList数组中包含milk', () => {
  expect(shoppingList).toContain('milk');
  expect(new Set(shoppingList)).toContain('milk');
});
  1. 例外

若你想测试某函数在调用时是否抛出了错误,你需要使用 toThrow

scss 复制代码
function compileAndroidCode() {
  throw new Error('you are using the wrong JDK!');
}

test('compiling android goes as expected', () => {
  expect(() => compileAndroidCode()).toThrow();
  expect(() => compileAndroidCode()).toThrow(Error);

  // You can also use a string that must be contained in the error message or a regexp
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
  expect(() => compileAndroidCode()).toThrow(/JDK/);

  // Or you can match an exact error message using a regexp like below
  expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK$/); // Test fails
  expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK!$/); // Test pass
});

测试异步代码

在JavaScript中执行异步代码是很常见的。 当你有以异步方式运行的代码时,Jest 需要知道当前它测试的代码是否已完成,然后它可以转移到另一个测试。

  1. Promise

为你的测试返回一个Promise,则Jest会等待Promise的resove状态 如果 Promise 的状态变为 rejected, 测试将会失败。

例如,有一个名为fetchData的Promise, 假设它会返回内容为'peanut butter'的字符串 我们可以使用下面的测试代码︰

kotlin 复制代码
test('the data is peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});
  1. Async/Await

如果期望Promise被Reject,则需要使用 .catch 方法。 请确保添加 expect.assertions 来验证一定数量的断言被调用。 否则,一个fulfilled状态的Promise不会让测试用例失败。

scss 复制代码
test('the data is peanut butter', async () => {
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    expect(e).toMatch('error');
  }
});
  1. 回调

默认情况下,一旦到达运行上下文底部Jest测试立即结束。 这样意味着这个测试将不能按预期工作。使用单个参数调用 done,而不是将测试放在一个空参数的函数。 Jest会等done回调函数被调用执行结束后,再结束测试。

scss 复制代码
test('the data is peanut butter', done => {
  function callback(error, data) {
    if (error) {
      done(error);
      return;
    }
    try {
      expect(data).toBe('peanut butter');
      done();
    } catch (error) {
      done(error);
    }
  }

  fetchData(callback);
});

钩子函数

  1. Repeating Setup

如果你有一些要为多次测试重复设置的工作,你可以使用 beforeEachafterEach

scss 复制代码
beforeEach(() => {
  initializeCityDatabase();
});

afterEach(() => {
  clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});
  1. 一次性设置

在某些情况下,你只需要在文件的开头做一次设置。 如果这个通用设置是异步的,就比较麻烦,因为没办法每个用例都设置一遍,这样性能还很差。你可以使用beforeAllafterAll

scss 复制代码
beforeAll(() => {
  return initializeCityDatabase();
});

afterAll(() => {
  return clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

Mock函数

Mock 函数允许你测试代码之间的连接------实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用 new 实例化时捕获构造函数的实例、允许测试时配置返回值。

有两种方法可以模拟函数:要么在测试代码中创建一个 mock 函数,要么编写一个手动 mock来覆盖模块依赖。

  1. 使用 mock 函数

假设我们要测试函数 forEach 的内部实现,这个函数为传入的数组中的每个元素调用一次回调函数。

ini 复制代码
export function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

为了测试此函数,我们可以使用一个 mock 函数,然后检查 mock 函数的状态来确保回调函数如期调用。

scss 复制代码
const forEach = require('./forEach');

const mockCallback = jest.fn(x => 42 + x);

test('forEach mock function', () => {
  forEach([0, 1], mockCallback);

  // The mock function was called twice
  expect(mockCallback.mock.calls).toHaveLength(2);

  // The first argument of the first call to the function was 0
  expect(mockCallback.mock.calls[0][0]).toBe(0);

  // The first argument of the second call to the function was 1
  expect(mockCallback.mock.calls[1][0]).toBe(1);

  // The return value of the first call to the function was 42
  expect(mockCallback.mock.results[0].value).toBe(42);
});
  1. Mock 的返回值
scss 复制代码
const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
  1. mock模块

假定有个从 API 获取用户的类。 该类用 axios 调用 API 然后返回 data,其中包含所有用户的属性:

javascript 复制代码
import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

export default Users;

我们可以用 jest.mock(...) 函数自动模拟 axios 模块。一旦模拟模块,我们可为 .get 提供一个 mockResolvedValue ,它会返回假数据用于测试。 实际上,我们想说的是我们想让axios.get('/users.json') 有个伪造的响应结果。

kotlin 复制代码
import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
  const users = [{name: 'Bob'}];
  const resp = {data: users};
  axios.get.mockResolvedValue(resp);

  // or you could use the following depending on your use case:
  // axios.get.mockImplementation(() => Promise.resolve(resp))

  return Users.all().then(data => expect(data).toEqual(users));
});
  1. 模拟部分模块

模块的子集可以被模拟,模块的其他部分可以维持当前实现:

dart 复制代码
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
scss 复制代码
//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';

jest.mock('../foo-bar-baz', () => {
  const originalModule = jest.requireActual('../foo-bar-baz');

  //Mock the default export and named export 'foo'
  return {
    __esModule: true,
    ...originalModule,
    default: jest.fn(() => 'mocked baz'),
    foo: 'mocked foo',
  };
});

test('should do a partial mock', () => {
  const defaultExportResult = defaultExport();
  expect(defaultExportResult).toBe('mocked baz');
  expect(defaultExport).toHaveBeenCalled();

  expect(foo).toBe('mocked foo');
  expect(bar()).toBe('bar');
});

工具库单元测试

如何搭建

  1. 创建jest.config.js
java 复制代码
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['<rootDir>/packages/utils/src/**/*.test.ts'],
  displayName: '工具库测试',
}
  • preset:决定你的文件被转换成什么语法
  • testEnvironment:测试环境,默认是node环境,组件库是需要jsdom环境
  • testMatch:决定jest去运行哪些文件,默认是__test__文件夹下的.js,.jsx,.ts,.tsx文件,同样.test或者.spec结尾的文件
  • displayName:终端输出提示
  1. 新建babel.config.js处理js文件
scss 复制代码
npm install --save-dev babel-jest @babel/core @babel/preset-env
lua 复制代码
module.exports = {
  presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};
  1. 处理ts文件

由于Babel没办法完美处理ts文件,会有一些边界case,所以这里采用的ts-jest + @types/jest,无需配置,开箱即用。

sql 复制代码
pnpm add ts-jest @types/jest -D

编写Demo

组件库单元测试

如何搭建

  1. 创建jest.config.js
java 复制代码
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  testMatch: ['<rootDir>/packages/components/src/**/*.test.ts', '<rootDir>/packages/components/src/**/*.test.tsx'],
  displayName: '组件库测试',
  moduleNameMapper: {
    '\.less$': 'identity-obj-proxy',
  }
}
  • moduleNameMapper:把匹配到的内容映射为你指定的内容。在前端的单元测试中,时常有许多内容是不需要的,比如:静态资源、样式文件等。
  1. 新建babel.config.js处理js文件

同上

  1. 处理ts文件

同上

  1. 处理jsx或者tsx文件

这里大概率会遇到版本依赖对应不上的问题,尤其是react版本低于17,解决也很简单就是找到对应的版本就好

sql 复制代码
pnpm add -D jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer

修改babel.config.js文件

css 复制代码
module.exports = {
  presets: [
    '@babel/preset-env',
    ['@babel/preset-react', {runtime: 'automatic'}],
  ],
};

如何编写

快照测试

每当你想要确保你的UI不会有意外的改变,快照测试是非常有用的工具。

典型的做法是在渲染了UI组件之后,保存一个快照文件, 检测他是否与保存在单元测试旁的快照文件相匹配。 若两个快照不匹配,测试将失败:有可能做了意外的更改,或者UI组件已经更新到了新版本。

javascript 复制代码
import renderer from 'react-test-renderer';
import Link from '../Link';

it('renders correctly', () => {
  const tree = renderer
    .create(<Link page="http://www.facebook.com">Facebook</Link>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

DOM测试

  1. 渲染
javascript 复制代码
import React from "react";
export default function Hello(props) {
  if (props.name) {
    return <h1>你好,{props.name}!</h1>;
  } else {
    return <span>嘿,陌生人</span>;
  }
}

在编写 UI 测试时,可以将渲染、用户事件或数据获取等任务视为与用户界面交互的"单元"。react-dom/test-utils 提供了一个名为 act() 的 helper,它确保在进行任何断言之前,与这些"单元"相关的所有更新都已处理并应用于 DOM:

ini 复制代码
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import Hello from "./hello";
let container = null;
beforeEach(() => {
  // 创建一个 DOM 元素作为渲染目标
  container = document.createElement("div");
  document.body.appendChild(container);
});
afterEach(() => {
  // 退出时进行清理unmountComponentAtNode(container);
  container.remove();
  container = null;
});
it("渲染有或无名称", () => {
  act(() => {
    render(<Hello />, container);
  });
  expect(container.textContent).toBe("嘿,陌生人");
  act(() => {
    render(<Hello name="Jenny" />, container);
  });
  expect(container.textContent).toBe("你好,Jenny!");
  act(() => {
    render(<Hello name="Margaret" />, container);
  });
  expect(container.textContent).toBe("你好,Margaret!");
});
  1. 事件
ini 复制代码
import React, { useState } from "react";

export default function Toggle(props) {
  const [state, setState] = useState(false);
  return (
    <button
      onClick={() => {
        setState((previousState) => !previousState);
        props.onChange(!state);
      }}
      data-testid="toggle"
    >
      {state === true ? "Turn off" : "Turn on"}
    </button>
  );
}

注意,你需要在创建的每个事件中传递 { bubbles: true } 才能到达 React 监听器,因为 React 会自动将事件委托给 root。

ini 复制代码
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Toggle from "./toggle";

let container = null;
beforeEach(() => {
  // 创建一个 DOM 元素作为渲染目标
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // 退出时进行清理
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("点击时更新值", () => {
  const onChange = jest.fn();
  act(() => {
    render(<Toggle onChange={onChange} />, container);
  });

  // 获取按钮元素,并触发点击事件
  const button = document.querySelector("[data-testid=toggle]");
  expect(button.innerHTML).toBe("Turn on");

  act(() => {
    button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });

  expect(onChange).toHaveBeenCalledTimes(1);
  expect(button.innerHTML).toBe("Turn off");

  act(() => {
    for (let i = 0; i < 5; i++) {
      button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    }
  });

  expect(onChange).toHaveBeenCalledTimes(6);
  expect(button.innerHTML).toBe("Turn on");
});

上面我们用的是React提供的单测函数,也有比较著名的单测库@testing-library/react可以帮助我们使用。原理上实际上差不多。

Monorepo单元测试搭建

如何搭建

  1. 配置文件
css 复制代码
module.exports = {
  preset: 'ts-jest',
  projects: [
    {
      preset: 'ts-jest',
      testEnvironment: 'node',
      testMatch: ['<rootDir>/packages/utils/src/**/*.test.ts'],
      displayName: '工具库测试',
    },
    {
      preset: 'ts-jest',
      testEnvironment: 'jsdom',
      testMatch: ['<rootDir>/packages/components/src/**/*.test.ts', '<rootDir>/packages/components/src/**/*.test.tsx'],
      displayName: '组件库测试',
      moduleNameMapper: {
        '\.less$': 'identity-obj-proxy',
      },
    },
  ],
  testEnvironment: 'node',
  transform: {
    '^.+\.[t|j]sx?$': 'ts-jest',
  },
  rootDir: '.',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
  globals: {
    'ts-jest': {
      tsConfig: 'tsconfig.json',
    },
  },
  collectCoverageFrom: [
    '<rootDir>/packages/utils/**/*.{js,jsx,ts,tsx}',
    '<rootDir>/packages/components/**/*.{js,jsx,ts,tsx}',
    '!**/demo/**',
    '!**/example/**',
    '!**/es/**',
    '!**/lib/**',
    '!**/dist/**',
    '!**/*.d.ts',
    '!**/_util/**',
    '!**/__tests__/**',
    '!**/types.ts',
  ],
}
  • projects:对应Monorepo的子项目
  • collectCoverageFrom:生成单测覆盖率报告
  1. 本地单测覆盖率解决方案

由于覆盖率生成的是静态资源,在本地没办法实时预览

php 复制代码
import { execSync } from 'child_process'

const generateReports = async () => {
  // 不知道为什么 需要先打开
  execSync(`open-cli http://127.0.0.1:8082 -- 'google chrome' --incognito`)
  execSync('jest --passWithNoTests --coverage --coverageProvider=v8 --silent', { stdio: 'inherit' })
  execSync('pnpm dlx http-server ./coverage/lcov-report -c-1 -p 8082', { stdio: 'inherit' })
  execSync(`open-cli http://127.0.0.1:8082 -- 'google chrome' --incognito`)
}

generateReports()

单测覆盖率

什么是测试覆盖率?用一个公式来表示:代码覆盖率 = 已执行的代码数 / 代码总数。Jest 如果要开启测试覆盖率统计,只需要在 Jest 命令后面加上 --coverage 参数:

json 复制代码
"scripts": {"test": "jest --coverage",}

现在我们测试用例看看测试覆盖率。

scss 复制代码
// main.js
function abs(a) {
  if (typeof a != "number") {
    throw new TypeError("参数必须为数值型");
  }
  if (a < 0) return -a;
  return a;
} 
// test.spec.js
test("abs", () => {
  expect(abs(1)).toBe(1);
  expect(abs(0)).toBe(0);
  expect(abs(-1)).toBe(1);
  expect(() => abs("abc")).toThrow(TypeError); // 类型错误
});

上图表示每一项覆盖率都是 100%。

现在我们把测试类型错误的那一行代码注释掉,再试试。

scss 复制代码
test("abs", () => {
  expect(abs(1)).toBe(1);
  expect(abs(0)).toBe(0);
  expect(abs(-1)).toBe(1);
  // expect(() => abs("abc")).toThrow(TypeError); // 类型错误
});

可以看到测试覆盖率下降了,为什么会这样呢?因为 abs() 函数中判断类型错误的那个分支的代码没有执行。

javascript 复制代码
// 就是这一个分支语句
if (typeof a != "number") {
    throw new TypeError("参数必须为数值型");
  }

覆盖率统计项

从覆盖率的图片可以看到一共有 4 个统计项:

  1. Stmts(statements):语句覆盖率,程序中的每个语句是否都已执行。
  2. Branch:分支覆盖率,是否执行了每个分支。
  3. Funcs:函数覆盖率,是否执行了每个函数。
  4. Lines:行覆盖率,是否执行了每一行代码。

可能有人会有疑问,1 和 4 不是一样吗?其实不一样,因为一行代码可以包含好几个语句。

javascript 复制代码
if (typeof a != "number") {
  throw new TypeError("参数必须为数值型");
}

if (typeof a != "number") throw new TypeError("参数必须为数值型");

例如上面两段代码,它们对应的测试覆盖率就不一样。现在把测试类型错误的那一行代码注释掉,再试试:

lua 复制代码
// expect(() => abs('abc')).toThrow(TypeError)

第一段代码对应的覆盖率

第二段代码对应的覆盖率

它们未执行的语句都是一样,但第一段代码 Lines 覆盖率更低,因为它有一行代码没执行。而第二段代码未执行的语句和判断语句是在同一行,所以 Lines 覆盖率为 100%。

相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax