使用Jasmine编写测试套件

学习目标

  • 介绍Jasmine测试框架

  • 编写测试套件、规范和断言

  • 使用"安排(Arrange)、行动(Act)、断言(Assert)"结构组织规范

  • 使用初始化(setup)和销毁(teardown)逻辑创建高效的测试套件

Angular内置了Jasmine,这是一个JavaScript框架,可以编写和执行单元测试和集成测试。Jasmine由三个重要部分组成:

  1. 一个包含用于构建测试的类和函数的库。

  2. 一个测试执行引擎。

  3. 一个将测试结果以不同格式输出的报告引擎。

如果您对Jasmine还不熟悉,建议阅读 官方的Jasmine教程。本指南对Jasmine进行了简要介绍,探讨了在本指南中将使用的基本结构和术语。

创建一个测试套件

在Jasmine中,测试由一个或多个 套件 组成。使用 describe 块声明一个套件:

describe('Suite description', () => {
  /* … */
});

每个套件 描述 了一段代码,即 被测试的代码

describe: 套件

describe 是一个接受两个参数的函数。

  1. 一个可读性强的字符串,通常是被测试的函数或类的名称。例如,describe('CounterComponent', /* … */) 是测试 CounterComponent 类的套件。

  2. 一个包含套件定义的函数。

describe 块将相关的规范(specs)分组,我们将在下一章节学习。

嵌套 describe

可以嵌套 describe 块以便将大的套件结构化,并将其分成逻辑上的部分:

describe('Suite description', () => {
  describe('One aspect', () => {
    /* … */
  });
  describe('Another aspect', () => {
    /* … */
  });
});

嵌套的 describe 块为一组规范(specs)添加了可读性强的描述。它们还可以拥有自己的初始化(setup)和销毁(teardown)逻辑。

规范(Specifications)

it: Spec

每个套件由一个或多个 规范(specifications) 组成,简称规范(specs)。使用 it 块声明一个规范:

describe('Suite description', () => {
  it('Spec description', () => {
    /* … */
  });
  /* … more specs …  */
});

再次强调,it 是一个函数,接受两个参数。第一个参数是一个可读性强的字符串,描述规范(spec)。第二个参数是一个包含规范代码的函数。

可读性强的句子

代词 it 指的是被测试的代码。it 应该是一个可读性强的句子的主语,用于断言被测试代码的行为。然后规范代码通过实现这个断言来证明其正确性。这种编写规范的风格源于行为驱动开发(Behavior-Driven Development, BDD)的概念。

BDD 的一个目标是以自然语言(在本例中是英语)描述软件行为。每个利益相关者都应该能够阅读 it 句子,并理解代码应该如何运行。没有 JavaScript 知识的团队成员应该能够通过构造 it does something 的句子来添加更多需求。

问问自己,被测试的代码做了什么?例如,对于一个 CounterComponentit 增加了计数器的值。并且 it 将计数器重置为特定的值。因此,您可以编写以下句子:

it('increments the count', () => {
  /* … */
});
it('resets the count', () => {
  /* … */
});

it 块之后,通常会跟随一个动词,例如在示例中的 incrementsresets

不使用 “should”

有些人喜欢写成 it('should increment the count', /* … */),但是 should 并没有额外的含义。规范的本质是陈述被测试代码应该做什么。使用 should 这个词只会增加冗余,让句子变得更长。本指南建议简单陈述代码的行为。

测试的结构

it 块内部是实际的测试代码。不论测试框架如何,测试代码通常包括三个阶段:安排(Arrange)、行动(Act)和断言(Assert)

安排、行动、断言
  1. Arrange 是准备和设置阶段。例如,被测试的类被实例化,依赖项被设置,间谍(spies)和伪装(fakes)被创建。

  2. Act 是与被测试代码进行交互的阶段。例如,调用一个方法或点击 DOM 中的一个 HTML 元素。

  3. Assert 是检查和验证代码行为的阶段。例如,将实际输出与预期输出进行比较。

针对 CounterComponent 的规范 it('resets the count', /* … */) 的结构可以是什么样子呢?

  1. Arrange

    • 创建一个 CounterComponent 的实例。

    • 将组件渲染到文档中。

  2. Act:

    • 找到并聚焦重置输入字段。

    • 输入文本“5”。

    • 找到并点击“重置”按钮。

  3. Assert:

    • 期望显示的计数现在为“5”。

组织一个测试

这个结构使得编写和实现测试更加容易。问问自己:

  • 需要哪些设置?我需要提供哪些依赖项?它们的行为是怎样的?(Arrange

  • 哪些用户输入或 API 调用会触发我想要测试的行为?(Act

  • 期望的行为是什么?如何证明这个行为是正确的?(Assert

Given, When, Then

在行为驱动开发(Behavior-Driven Development BDD)中,测试的三个阶段本质上是相同的。但它们被称为 给定(Given)、当(When)和那么(Then)。这些简单的英语单词试图避免技术术语,并提供了一种自然的思考测试结构的方式:“给定(Given) 这些条件,当(when) 用户与应用程序交互时,那么(then) 它会以某种方式表现。”

期望

断言 阶段,测试将实际输出或返回值与期望的输出或返回值进行比较。如果它们相同,测试通过。如果它们不同,测试失败。

让我们来看一个简单的人为示例,一个 add 函数:

const add = (a, b) => a + b;

一个没有任何测试工具的原始测试可以是这样的:

const expectedValue = 5;
const actualValue = add(2, 3);
if (expectedValue !== actualValue) {
  throw new Error(
    `Wrong return value: ${actualValue}. Expected: ${expectedValue}`
  );
}
expect

我们可以在 Jasmine 规范中编写该代码,但是 Jasmine 允许我们以一种更简单、更简洁的方式创建期望值:使用 expect 函数和匹配器(matcher)。

const expectedValue = 5;
const actualValue = add(2, 3);
expect(actualValue).toBe(expectedValue);

首先,我们将实际值传递给 expect 函数。它返回一个期望对象,其中包含用于检查实际值的方法。我们想要将实际值与期望值进行比较,因此我们使用了 toBe 匹配器。

匹配器(Matchers)

toBe 是适用于所有可能的 JavaScript 值的最简单的匹配器。它在内部使用了 JavaScript 的严格相等运算符 ===expect(actualValue).toBe(expectedValue) 本质上运行的是 actualValue === expectedValue

toBe 适用于比较字符串、数字和布尔值等基本类型。对于对象,toBe 仅在实际值和期望值完全相同的对象时匹配。即使两个对象恰好具有相同的属性和值,如果它们不是完全相同的对象,toBe 会失败。

如果要检查两个对象的深层相等性,Jasmine 提供了 toEqual 匹配器。下面的示例说明了它们之间的区别:

// Fails, the two objects are not identical
expect({ name: 'Linda' }).toBe({ name: 'Linda' });

// Passes, the two objects are not identical but deeply equal
expect({ name: 'Linda' }).toEqual({ name: 'Linda' });

Jasmine内置了许多有用的匹配器,其中 toBetoEqual 是最常用的。您可以添加自定义匹配器,将复杂的检查隐藏在一个简短的名称后面。

可读的句子

模式 expect(actualValue).toEqual(expectedValue) 再次源自行为驱动开发(BDD)。expect 函数调用和匹配器方法形成了一个可读的句子:“期望实际值等于期望值”。目标是编写一个规范,它像纯文本一样可读,但可以自动验证。

高效的测试套件

在一个套件中编写多个规范时,您很快会意识到 Arrange 阶段在这些规范中是相似或甚至相同的。例如,当测试 CounterComponent 时, Arrange 阶段始终包括创建一个组件实例并将其渲染到文档中。

重复的设置

这种设置反复出现,因此应该在一个中心位置定义一次。您可以编写一个 setup 函数,并在每个规范的开头调用它。但是使用 Jasmine,您可以声明在每个规范之前和之后,或在所有规范之前和之后调用的代码。

为实现这个目的,Jasmine 为我们提供了四个函数:beforeEachafterEachbeforeAllafterAll。它们在 describe 块内部调用,就像 it 一样。它们都接受一个参数,即在给定阶段调用的函数。

describe('Suite description', () => {
  beforeAll(() => {
    console.log('Called before all specs are run');
  });
  afterAll(() => {
    console.log('Called after all specs are run');
  });

  beforeEach(() => {
    console.log('Called before each spec is run');
  });
  afterEach(() => {
    console.log('Called after each spec is run');
  });

  it('Spec 1', () => {
    console.log('Spec 1');
  });
  it('Spec 2', () => {
    console.log('Spec 2');
  });
});

这个套件有两个规范,并定义了共享的初始化(setup)和销毁(teardown)代码。输出结果如下:

Called before all specs are run
Called before each spec is run
Spec 1
Called after each spec is run
Called before each spec is run
Spec 2
Called after each spec is run
Called after all specs are run

我们要编写的大多数测试都将包含一个 beforeEach 块来承载安排(Arrange)代码。