使用Jasmine编写测试套件
学习目标
-
介绍Jasmine测试框架
-
编写测试套件、规范和断言
-
使用"安排(Arrange)、行动(Act)、断言(Assert)"结构组织规范
-
使用初始化(setup)和销毁(teardown)逻辑创建高效的测试套件
Angular内置了Jasmine,这是一个JavaScript框架,可以编写和执行单元测试和集成测试。Jasmine由三个重要部分组成:
-
一个包含用于构建测试的类和函数的库。
-
一个测试执行引擎。
-
一个将测试结果以不同格式输出的报告引擎。
如果您对Jasmine还不熟悉,建议阅读 官方的Jasmine教程。本指南对Jasmine进行了简要介绍,探讨了在本指南中将使用的基本结构和术语。
创建一个测试套件
在Jasmine中,测试由一个或多个 套件
组成。使用 describe
块声明一个套件:
describe('Suite description', () => {
/* … */
});
每个套件 描述 了一段代码,即 被测试的代码。
describe : 套件
|
describe
是一个接受两个参数的函数。
-
一个可读性强的字符串,通常是被测试的函数或类的名称。例如,
describe('CounterComponent', /* … */)
是测试CounterComponent
类的套件。 -
一个包含套件定义的函数。
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
的句子来添加更多需求。
问问自己,被测试的代码做了什么?例如,对于一个 CounterComponent
,it 增加了计数器的值。并且 it 将计数器重置为特定的值。因此,您可以编写以下句子:
it('increments the count', () => {
/* … */
});
it('resets the count', () => {
/* … */
});
在 it
块之后,通常会跟随一个动词,例如在示例中的 increments
和 resets
。
不使用 “should” |
有些人喜欢写成 it('should increment the count', /* … */)
,但是 should
并没有额外的含义。规范的本质是陈述被测试代码应该做什么。使用 should 这个词只会增加冗余,让句子变得更长。本指南建议简单陈述代码的行为。
测试的结构
在 it
块内部是实际的测试代码。不论测试框架如何,测试代码通常包括三个阶段:安排(Arrange)、行动(Act)和断言(Assert)。
安排、行动、断言 |
-
Arrange 是准备和设置阶段。例如,被测试的类被实例化,依赖项被设置,间谍(spies)和伪装(fakes)被创建。
-
Act 是与被测试代码进行交互的阶段。例如,调用一个方法或点击 DOM 中的一个 HTML 元素。
-
Assert 是检查和验证代码行为的阶段。例如,将实际输出与预期输出进行比较。
针对 CounterComponent
的规范 it('resets the count', /* … */)
的结构可以是什么样子呢?
-
Arrange:
-
创建一个
CounterComponent
的实例。 -
将组件渲染到文档中。
-
-
Act:
-
找到并聚焦重置输入字段。
-
输入文本“5”。
-
找到并点击“重置”按钮。
-
-
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内置了许多有用的匹配器,其中 toBe
和 toEqual
是最常用的。您可以添加自定义匹配器,将复杂的检查隐藏在一个简短的名称后面。
可读的句子 |
模式 expect(actualValue).toEqual(expectedValue)
再次源自行为驱动开发(BDD)。expect
函数调用和匹配器方法形成了一个可读的句子:“期望实际值等于期望值”。目标是编写一个规范,它像纯文本一样可读,但可以自动验证。
高效的测试套件
在一个套件中编写多个规范时,您很快会意识到 Arrange 阶段在这些规范中是相似或甚至相同的。例如,当测试 CounterComponent
时, Arrange 阶段始终包括创建一个组件实例并将其渲染到文档中。
重复的设置 |
这种设置反复出现,因此应该在一个中心位置定义一次。您可以编写一个 setup
函数,并在每个规范的开头调用它。但是使用 Jasmine,您可以声明在每个规范之前和之后,或在所有规范之前和之后调用的代码。
为实现这个目的,Jasmine 为我们提供了四个函数:beforeEach
、afterEach
、beforeAll
和 afterAll
。它们在 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)代码。