行业实用工具

从根本上讲,自动化测试只是在出现错误时会抛出错误或导致错误的的代码。大多数库或测试框架都提供了各种基元,使测试更易于编写。

如上一节所述,这些基元几乎总是包含一种定义独立测试(称为测试用例)和提供断言的方法。断言是将检查结果和在出现错误时抛出错误相结合的一种方法,并且可以被认为是所有测试基元的基本基元。

本页介绍了针对这些基元的一般方法。您选择的框架可能具有类似的功能,但这不是确切的参考。

例如

import { fibonacci, catalan } from '../src/math.js';
import { assert, test, suite } from 'a-made-up-testing-library';

suite('math tests', () => {
  test('fibonacci function', () => {
    // check expected fibonacci numbers against our known actual values
    // with an explanation if the values don't match
    assert.equal(fibonacci(0), 0, 'Invalid 0th fibonacci result');
    assert.equal(fibonacci(13), 233, 'Invalid 13th fibonacci result');
  });
  test('relationship between sequences', () => {
    // catalan numbers are greater than fibonacci numbers (but not equal)
    assert.isAbove(catalan(4), fibonacci(4));
  });
  test('bugfix: check bug #4141', () => {
    assert.isFinite(fibonacci(0)); // fibonacci(0) was returning NaN
  })
});

此示例创建了一组测试(有时称为套件),名为“数学测试”,并定义了三个独立的测试用例,每个测试用例都运行一些断言。这些测试用例通常可以单独寻址或运行,例如,通过测试运行器中的过滤器标志。

作为基元的断言帮助程序

大多数测试框架(包括 Vitest)都在 assert 对象上包含断言帮助程序集合,使您可以根据某些预期快速检查返回值或其他状态。该预期通常是“已知良好”的值。在前面的示例中,我们知道第 13 个斐波那契数应该是 233,因此我们可以使用 assert.equal 直接确认这一点。

您可能还期望值采用某种形式,或者大于另一个值,或者具有某些其他属性。本课程不会涵盖所有可能的断言帮助程序,但测试框架始终至少提供以下基本检查

  • 一个 “真值”检查,通常描述为“ok”检查,检查条件是否为真,这与您可能编写的检查某事物是否成功或正确的 if 语句相匹配。这通常以 assert(...)assert.ok(...) 的形式提供,并接受单个值加上可选注释。

  • 相等性检查,例如在数学测试示例中,您期望对象的返回值或状态等于已知良好值。这些用于原始相等性(例如数字和字符串)或引用相等性(这些是同一个对象吗?)。在底层,这些只是带有 ===== 比较的“真值”检查。

    • JavaScript 区分松散 (==) 相等和严格 (===) 相等。大多数测试库为您提供方法 assert.equalassert.strictEqual
  • 深度相等性检查,它扩展了相等性检查,以包括检查对象、数组和其他更复杂数据类型的内容,以及遍历对象以进行比较的内部逻辑。这些非常重要,因为 JavaScript 没有内置方法来比较两个对象或数组的内容。例如,[1,2,3] == [1,2,3] 始终为 false。测试框架通常包括 deepEqualdeepStrictEqual 帮助程序。

比较两个值(而不仅仅是“真值”检查)的断言帮助程序通常采用两个或三个参数

  • 从被测代码生成或描述要验证的状态的实际值。
  • 预期值,通常是硬编码的(例如,文字数字或字符串)。
  • 描述预期内容或可能失败原因的可选注释,如果此行失败,则将包含该注释。

组合断言以构建各种检查也是相当常见的做法,因为很少有单个断言可以正确地确认系统的状态。例如

  test('JWT parse', () => {
    const json = decodeJwt('eyJieSI6InNhbXRob3Ii…');

    assert.ok(json.payload.admin, 'user should be admin');
    assert.deepEqual(json.payload.groups, ['role:Admin', 'role:Submitter']);
    assert.equal(json.header.alg, 'RS265')
    assert.isAbove(json.payload.exp, +new Date(), 'expiry must be in future')
  });

Vitest 在内部使用 Chai 断言库 来提供其断言帮助程序,并且查看其参考资料以了解哪些断言和帮助程序可能适合您的代码可能会很有用。

Fluent 和 BDD 断言

一些开发者更喜欢可以称为行为驱动开发 (BDD) 或 Fluent 样式断言的断言样式。这些也称为“expect”帮助程序,因为检查预期的入口点是名为 expect() 的方法。

Expect 帮助程序的行为方式与编写为简单方法调用的断言(如 assert.okassert.strictDeepEquals)相同,但一些开发者发现它们更易于阅读。BDD 断言可能如下所示

// A failure here would generate "Expect result to be an array that does include 42"
const result = await possibleMeaningsOfLife();
expect(result).to.be.an('array').that.does.include(42);

// or a simpler form
expect(result).toBe('array').toContainEqual(42);

// the same in assert might be
assert.typeOf(result, 'array', 'Expected the result to be an array');
assert.include(result, 42, 'Expected the result to include 42');

这些样式的断言之所以有效,是因为一种称为方法链的技术,其中 expect 返回的对象可以与进一步的方法调用持续链接在一起。调用的一些部分,包括上一个示例中的 to.bethat.does,没有功能,仅包含这些部分是为了使调用更易于阅读,并有可能在测试失败时生成自动注释。(值得注意的是,expect 通常不支持可选注释,因为链接应清楚地描述失败。)

许多测试框架都支持 Fluent/BDD 和常规断言。例如,Vitest 导出了 Chai 的两种方法,并且具有自己稍微简洁的 BDD 方法。另一方面,Jest 默认情况下仅包含 expect 方法

跨文件分组测试

在编写测试时,我们已经倾向于提供隐式分组,而不是将所有测试放在一个文件中,而是通常跨多个文件编写测试。实际上,测试运行器通常只知道文件用于测试,因为存在预定义的过滤器或正则表达式,例如,vitest 包括项目中所有扩展名如“.test.jsx”或“.spec.ts”(“.test”和“.spec”加上一些有效扩展名)的文件。

组件测试倾向于位于被测组件的同级文件中,如下面的目录结构所示

A list of files in a
  directory, including UserList.tsx and UserList.test.tsx.
组件文件和相关的测试文件。

同样,单元测试倾向于放在被测代码旁边。端到端测试可以各自位于自己的文件中,集成测试甚至可以放在其自己唯一的文件夹中。当复杂的测试用例增长到需要自己的非测试支持文件(例如,仅测试所需的支持库)时,这些结构可能很有用。

在文件中分组测试

如前面的示例中所用,通常的做法是将测试放在对 suite() 的调用中,该调用将您使用 test() 设置的测试分组在一起。套件通常不是测试本身,但它们通过调用传递的方法来帮助通过对相关测试或目标进行分组来提供结构。对于 test(),传递的方法描述了测试本身的操作。

与断言一样,在 Fluent/BDD 中,分组测试也存在相当标准的等效项。以下代码比较了一些典型的示例

// traditional/TDD
suite('math tests', () => {
  test('handle zero values', () => {
    assert.equal(fibonacci(0), 0);
  });
});

// Fluent/BDD
describe('math tests', () => {
  it('should handle zero values', () => {
    expect(fibonacci(0)).toBe(0);
  });
})

在大多数框架中,suitedescribe 的行为类似,testit 的行为也类似,这与使用 expectassert 编写断言之间的更大差异相反。

其他工具对排列套件和测试有细微不同的方法。例如,Node.js 的内置测试运行器支持嵌套调用 test() 以隐式创建测试层次结构。但是,Vitest 仅允许使用 suite() 进行此类嵌套,并且不会运行在另一个 test() 中定义的 test()

就像断言一样,请记住,您的技术堆栈提供的分组方法的精确组合并不那么重要。本课程将抽象地介绍它们,但您需要弄清楚它们如何应用于您选择的工具。

生命周期方法

分组测试的一个原因(即使在文件中的顶层隐式分组)是提供为每个测试运行或为一组测试运行一次的设置和拆卸方法。大多数框架提供四种方法

对于每个 `test()` 或 `it()` 对于套件运行一次
在测试运行之前 `beforeEach()` `beforeAll()`
在测试运行之后 `afterEach()` `afterAll()`

例如,您可能希望在每次测试之前预填充虚拟用户数据库,并在之后清除它

suite('user test', () => {
  beforeEach(() => {
    insertFakeUser('bob@example.com', 'hunter2');
  });
  afterEach(() => {
    clearAllUsers();
  });

  test('bob can login', async () => {  });
  test('alice can message bob', async () => {  });
});

这对于简化测试很有用。您可以共享通用的设置和拆卸代码,而不是在每个测试中重复它。此外,如果设置和拆卸代码本身抛出错误,则可能表明存在不涉及测试本身失败的结构性问题。

一般建议

以下是一些在考虑这些基元时要记住的提示。

基元是一个指南

请记住,此处的工具和基元以及接下来几页中的工具和基元与 Vitest、Jest、Mocha、Web Test Runner 或任何其他特定框架并不完全匹配。虽然我们已将 Vitest 用作一般指南,但请务必将它们映射到您选择的框架。

根据需要混合和匹配断言

从根本上讲,测试是可以抛出错误的代码。每个运行器都将提供一个基元(可能是 test())来描述不同的测试用例。

但是,如果该运行器还提供 assert()expect() 和断言帮助程序,请记住,这部分更多的是关于便利性,如果需要,您可以跳过它。您可以运行任何可能抛出错误的代码,包括其他断言库或老式的 if 语句。

IDE 设置可以成为救命稻草

确保您的 IDE(如 VSCode)可以访问您选择的测试工具的自动完成和文档可以提高您的工作效率。例如,Chai 断言库中的 assert 上有 100 多种方法,并且内联显示正确方法的文档会很方便。

对于某些使用其测试方法填充全局命名空间的测试框架,这可能尤其重要。这是一个细微的差别,但通常可以在不导入它们的情况下使用测试库(如果它们已自动添加到全局命名空间中)

// some.test.js
test('using test as a global', () => {  });

我们建议导入帮助程序,即使它们是自动支持的,因为这为您的 IDE 提供了一种查找这些方法的清晰方法。(您在构建 React 时可能遇到过此问题,因为某些代码库具有神奇的 React 全局变量,但有些代码库没有,并且需要在所有使用 React 的文件中导入它。)

// some.test.js
import { test } from 'vitest';
test('using test as an import', () => {  });