测试
当使用 Ionic CLI 生成 @ionic/angular 应用时,它会自动配置好应用的单元测试和端到端测试环境。这与 Angular CLI 使用的设置相同。有关测试 Angular 应用的详细信息,请参阅 Angular 测试指南。
测试原则
测试应用时,最好记住:测试可以揭示系统中是否存在缺陷 。然而,要证明任何非平凡的系统完全没有缺陷是不可能的。因此,测试的目标不是验证代码的正确性,而是发现代码中的问题。这是一个微妙但重要的区别。
如果我们旨在证明代码是正确的,我们更可能只走代码的“快乐路径”。如果我们旨在发现问题,我们则更可能全面地执行代码,找到潜藏的错误。
同样,最好从一开始就进行应用测试。这允许在流程早期发现缺陷,此时修复它们更容易。这也允许在向系统添加新功能时,有信心地进行代码重构。
单元测试
单元测试在隔离环境中执行代码的单个单元(组件、页面、服务、管道等)。通过注入模拟对象(mock)来替代代码的依赖项实现隔离。模拟对象允许测试精细控制依赖项的输出。模拟对象还允许测试确定哪些依赖项被调用过以及向它们传递了什么。
编写良好的单元测试结构清晰,通过 describe() 回调来描述代码单元及其包含的功能。代码单元及其功能的要求通过 it() 回调进行测试。当阅读 describe() 和 it() 回调的描述时,它们应该构成一个有意义的短语。当嵌套的 describe() 和最终的 it() 的描述连接在一起时,它们形成一个完整描述测试用例的句子。
由于单元测试在隔离环境中执行代码,因此它们快速、健壮,并且允许实现高代码覆盖率。
使用模拟对象 (Mocks)
单元测试在隔离环境中执行代码模块。为方便起见,我们建议使用 Jasmine (https://jasmine.github.io/)。Jasmine 创建模拟对象(Jasmine 称之为 "spies")来在测试时代替依赖项。当使用模拟对象时,测试可以控制对该依赖项调用返回的值,使当前测试独立于对依赖项所做的更改。这也简化了测试设置,使测试只关注被测模块内的代码。
使用模拟对象还允许测试查询模拟对象,通过 toHaveBeenCalled* 系列函数来确定它是否被调用以及如何被调用。测试应尽可能具体地使用这些函数,在测试方法是否被调用时,优先使用 toHaveBeenCalledTimes 而不是 toHaveBeenCalled。也就是说 expect(mock.foo).toHaveBeenCalledTimes(1) 比 expect(mock.foo).toHaveBeenCalled() 更好。在测试某些内容未被调用时,则应遵循相反的建议(expect(mock.foo).not.toHaveBeenCalled())。
在 Jasmine 中有两种常见的创建模拟对象的方法。可以使用 jasmine.createSpy 和 jasmine.createSpyObj 从头构建模拟对象,或者可以使用 spyOn() 和 spyOnProperty() 将 spies 安装到现有对象上。
使用 jasmine.createSpy 和 jasmine.createSpyObj
jasmine.createSpyObj 从头创建一个完整的模拟对象,并在创建时定义一组模拟方法。这非常有用,因为它非常简单。无需在测试 中构造或注入任何东西。使用此函数的缺点是它可能创建与真实对象不匹配的对象。
jasmine.createSpy 类似,但它创建一个独立的模拟函数。
使用 spyOn() 和 spyOnProperty()
spyOn() 将 spy 安装到现有对象上。使用这种技术的优点是,如果尝试监视对象上不存在的方法,则会引发异常。这防止了测试模拟不存在的方法。缺点是测试需要从一个完全成形的对象开始,这可能会增加所需的测试设置量。
spyOnProperty() 类似,区别在于它监视的是属性而不是方法。
通用测试结构
单元测试包含在 spec 文件中,每个实体(组件、页面、服务、管道等)对应一个 spec 文件。spec 文件与其测试的源代码放在一起,并以源代码的名称命名。例如,如果项目有一个名为 WeatherService 的服务,其代码位于 weather.service.ts 文件中,测试则位于同一文件夹下的 weather.service.spec.ts 文件中。
spec 文件本身包含一个 describe 调用来定义整体测试。在其内部嵌套其他 describe 调用来定义主要功能区域。每个 describe 调用可以包含设置和清理代码(通常通过 beforeEach 和 afterEach 调用处理)、更多的 describe 调用(形成功能 的层次结构分解)以及定义各个测试用例的 it 调用。
describe 和 it 调用还包含一个描述性文本标签。在结构良好的测试中,describe 和 it 调用与其标签组合成恰当的短语,并且每个测试用例的完整标签(由 describe 和 it 标签组合而成)形成一个完整的句子。
例如:
describe('计算', () => {
describe('除法', () => {
it('正确计算 4 / 2', () => {});
it('拒绝除以零', () => {});
...
});
describe('乘法', () => {
...
});
});
外层的 describe 调用表明正在测试 计算 服务,内层的 describe 调用确切说明正在测试什么功能,而 it 调用说明测试用例是什么。运行时,每个测试用例的完整标签是一个有意义的句子(计算 除法 拒绝除以零)。
页面和组件
页面就是 Angular 组件。因此,页面和组件都使用 Angular 的组件测试指南进行测试。
由于页面和组件包含 TypeScript 代码和 HTML 模板标记,因此可以执 行组件类测试和组件 DOM 测试。创建页面时,生成的模板测试如下所示:
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TabsPage } from './tabs.page';
describe('TabsPage', () => {
let component: TabsPage;
let fixture: ComponentFixture<TabsPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TabsPage],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
fixture = TestBed.createComponent(TabsPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('应该创建', () => {
expect(component).toBeTruthy();
});
});
在执行组件类测试时,通过 component = fixture.componentInstance; 定义的组件对象来访问组件。这是组件类的一个实例。在执行 DOM 测试时,使用 fixture.nativeElement 属性。这是组件的实际 HTMLElement,允许测试使用标准 HTML API 方法(如 HTMLElement.querySelector)来检查 DOM。