交易工具

从本质上讲,自动化测试只是一些代码,如果出现问题,就会抛出或导致错误。大多数库或测试框架都提供各种基元,可让您更轻松地编写测试。

如上一部分中所述,这些基元几乎总是包含定义独立测试(称为“测试用例”)和提供断言的方法。断言是一种结合使用检查结果和在发生错误时抛出错误的方式,可视为所有测试基元的基本基元。

本页将介绍处理这些基元的一般方法。您选择的框架可能有类似的内容,但这不是确切的引用。

例如:

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 对象上包含一组断言帮助程序,可让您根据某些expectation快速检查返回值或其他状态。期望值通常是“已知的良好”值。在前面的示例中,我们知道第 13 个斐波那契数应为 233,因此我们可以直接使用 assert.equal 进行确认。

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

  • 'actualy' 检查(通常被描述为“正常”检查),用于检查某个条件是否成立,类似于您编写 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() 的方法。

期望帮助程序的行为方式与编写为 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”以及大量有效的扩展名)。

组件测试往往位于被测组件的对等文件中,如以下目录结构所示:

目录中的文件列表,包括 UserList.tsx 和 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 断言库中有超过 100 种有关 assert 的方法,将正确方法的文档显示为内嵌会非常方便。

对于某些使用测试方法填充全局命名空间的测试框架来说,这一点尤为重要。两者之间只有细微的差别,但如果是自动添加到全局命名空间,则通常可以在不导入测试库的情况下使用它们:

// 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', () => { … });