前言
在实际的开发中,讲究高效性和实用性,所以组件库开发调试完成后,需要为每个组件编写对应的单元测试代码,并在做了代码审查和测试审查的基础上,达到高的代码覆盖率(即代码的覆盖程度,是一种度量方式)⽬标,从而更好的加快项目进度、提升代码质量。
本文主要是整理了写Angular单元测试的一些基础知识点以及如何为通用组件编写单元测试代码。
一、 单元测试是什么?
隔离程序的每个部件,单独运行测试用例,即保证代码指令,避免代码出现bug的一种测试方式。单元测试中,所写的测试需要事先提供既定的输入值与相应的逻辑单元,检测输出的结果与预期结果是否匹配。
二、配置jasmine & karma
Angular 官方提供了测试工具是 Jasmine 和 Karma,其中 jasmine 是 Angular 单元测试使用的测试框架,karma 是测试过程的管理工具,主要记录测试的过程以及反馈输出。
Angular CLI 会下载并安装试用 Jasmine 测试框架测试 Angular 应用时所需的一切。使用CLI创建的项目是可以立即用于测试的,运行CLI命令 ng test 即可。ng test 命令在*监视模式*下构建应用,并启动 Karma 测试运行器。
CLI 会生成 Jasmine 和 Karma 的配置文件,主要的配置文件包括 karma.conf.js 和 test.ts。
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
- 确保根目录下有 karma.conf.js 配置文件,它是为了告知 Karma 需要启用哪些插件、加载哪些测试脚本、需要哪些测试浏览器环境、测试报告通知方式、日志等等。
- 确保 src 下有 test.ts 文件,它是angular.json 中指定的测试入口文件,不仅初始化了测试环境,还指定了所有的测试文件。
- 测试文件的扩展名必须是 .spec.ts,这样工具才能识别出它是一个测试(或“规约”)文件。
三、技术点
1. 变更监测detectChanges
Angular单元测试中,经常需要调用 fixture.detectChanges() 强制触发变化检测,如果不调用,模板中绑定的数据就不会更新。
Angular 通过 NgZone 控制可能造成模板数据更新的点,然后在合适的时候触发变化检测;然而测试环境中没有 NgZone,所以便需要手动调用变化检测。
2. 模拟异步fakeAsync
程序中有异步操作是很常见的,比如setTimeout、Promise.then等,如果需要测试异步执行完成之后的结果,就需要用到 tick,而 tick 就必须运行在 fakeAsync 中。
tick() 是模拟异步时间片的定时器,可以设置一个等待时长的参数(单位:毫秒),通过在 fakeAsync 测试区域中刷新定时器和微任务队列来仿真时间的流逝以及异步活动的完成。
flush() 也是模拟异步时间片的定时器,flush() 的结束条件不是一个时间值,而是微任务队列为空,例如Promise.then 的场景(ngModel 赋值触发的异步),就可以使用flush()。
3. Spy
Spy 能够追踪函数的调用历史信息(是否被调用、调用参数列表、被请求次数等),所以jasmine 用它来模拟和监听函数调用。
Spy 仅存在于定义它的 describe 和 it 方法块中,并且每次在 spec 执行完之后被销毁。
四、单元测试基础结构
为大家理解的更清楚,先看下面一个测试文件的部分代码,然后再逐一讲解。
describe("SearchConditionComponent", () => {
let component: SearchConditionComponent;
let fixture: ComponentFixture<SearchConditionComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SearchConditionComponent],
providers: [
{
provide: ComponentFixtureAutoDetect,
useValue: true,
},
],
}).compileComponents();
fixture = TestBed.createComponent(SearchConditionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it("should create", () => {
expect(component).toBeTruthy();
});
it("校验默认初始值", () => {
expect(component.keySetting.labelField).toBe("label");
expect(component.keySetting.valueField).toBe("value");
});
}
1. describe
定义整体测试描述的方法,第一个参数表示描述、第二个参数是一个函数,在函数中我们进行测试。
describe 是 Jasmine 的全局函数,可以理解为一个测试集,主要功能是用来划分单元测试。可以嵌套使用,并且describe 定义的变量对该测试集中任何 it 代码块都是可见的。
2. beforeEach\afterEach
写单元测试公共逻辑的地方,公共逻辑相对于每个it都是独立的,不会在多个测试用例之间共享任何数据。
它们分别在每个it执行之前和之后执行,afterEach 可以用来清除每个测试用例执行过程中产生的一些副作用,比如测试中需要给全局的服务初始化一些数据,那么就需要在 afterEach 中把初始化的数据清空。
3. it
定义单元测试方法,第一个参数表示测试功能点、第二个参数是函数,在里面我们定义断言方法。it 是单元测试的最小单元, 对应于一个测试用例,每个 spec 包含一个或多个 expectations 期望值来测试需要测试的代码。测试用例大概的逻辑:手动构造相关参数,然后在测试用例中调用该方法,写出理想的预期。
4. expect
定义断言方法,参数就是断言表达式,提供了针对值、spy等相关场景的断言。Jasmine 中每个 expectation 是一个断言,可以是 true 或者 false。当每个 spec 中的所有 expectations 都是 true,则通过测试,有任何一个 expectation 是 false,则未通过测试。
每个 matchers 实现一个布尔值,在实际值和期望值之间比较。它负责通知 Jasmine,此 expectation 是真或者假。然后 Jasmine 会认为相应的 spec 是通过还是失败。所有的 expect 都可以使用 not 表示否定的断言。
4.1 断言方法
- toBe 类似于
===
- toEqual 比较变量字面量的值
- toMatch 匹配值与正则表达式
- toBeDefined 检验变量是否定义
- toBeNull 检验变量是否为
null
- toBeTruthy 检查变量值是否能转换成布尔型
真
值 - toBeFalsy 检查变量值是否能转换成布尔型
假
值 - toContain 检查在数组中是否包含某个元素
- toBeLessThan 检查变量是否小于某个数字
- toBeGreaterThan 检查变量是否大于某个数字或者变量
- toBeCloseTo 比较两个数在保留几位小数位之后,是否相等,用于数字的精确比较
- toThrow 检查一个函数是否会throw异常
- toHaveBeenCalled 检查一个监听函数是否被调用过
- toHaveBeenCalledWith 检查监听函数调用时的参数匹配信息
5. configureTestingModule
创建初始测试环境,为测试配置模块 Module,需要把该测试组件用到的依赖 Module、服务以及测试组件本身的定义统统配置好,否则测试是无法正常运行的。
6. compileComponents
在配置好测试模块之后,异步编译它。调用完 compileComponents 之后,TestBed 的配置就会在当前测试期间被冻结。
7. createComponent
配置 TestBed 后,便可以调用它的 createComponent() 方法。TestBed.createComponent() 会创建组件的实例,把对应元素添加到测试运行器的 DOM 中,并返回一个 ComponentFixture 对象。
8. ComponentFixture
ComponentFixture 是访问测试组件的入口,用于与所创建的组件及其对应的元素进行交互。可以通过测试夹具(fixture)访问组件实例,并用 Jasmine 的期望断言来确认它是否存在。
let fixture: ComponentFixture<SearchConditionComponent>;
fixture = TestBed.createComponent(SearchConditionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
8.1 创建固件
一般需要在 beforeEach 中创建测试组件的实例,因为每个测试用例都需要对 Angular 组件进行创建、销毁,Angular 对这些操作进行了封装,通过 TestBed 创建组件口返回的对象就是一个ComponentFixture 类型。那么在后续的测试代码中,便可以直接使用 fixture 对象对组件进行各种验证。
8.2 常用属性
(1)componentInstance:获取测试组件实例
(2)debugElement:返回测试根组件的DebugElement实例,通过该实例可以访问(查询)fixture 的整个元素和组件子树。
(3)nativeElement:可以直接fixture.nativeElement提供组件的元素,但这是一种便利方法,其最终实现还是fixture.debugElement.nativeElement。
Q:为什么使用这种迂回的路径访问元素呢???
nativeElement 的属性依赖运行时的环境,若在非浏览器平台上运行这些测试,这些平台上可能没有 DOM,或者其模拟的 DOM 不支持完整的 HTMLElement API,该怎么办呢?
Angular依靠 DebugElement 抽象来在其支持的所有平台上安全地工作,不会创建 HTML 元素树,而会创建一个 DebugElement 树来封装运行时平台上的原生元素,最后再通过 nativeElement属性解包 DebugElement,返回特定于平台的元素对象。
8.3 常用方法
detectChanges():强制执行一次测试的组件变化检测,一般会在组件创建完成后强制执行一次变化检测。
8.4 By.css()
虽然测试都是在浏览器中运行的,但有些应用可能至少要在某些时候运行在不同的平台上。比如,作为优化策略的一部分,某组件可能会首先在服务器上渲染,以便在连接不良的设备上更快地启动应用。而服务器端渲染器可能不支持完整 HTML 元素 API,若不支持 querySelector,之前的测试就会失败。
DebugElement 提供了适用于其支持的所有平台的查询方法。这些查询方法接受一个谓词函数,当 DebugElement 树中的一个节点与选择条件匹配时,该函数返回 true。
借助从浏览器平台为运行时平台导入 By 类来创建一个谓词如下:
import { By } from "@angular/platform-browser";
下面的例子用 DebugElement.queryall() 和浏览器的 By.css() 实现测试。
fixture.detectChanges();
const elem: DebugElement = fixture.debugElement;
const iconDebug = elem.queryAll(By.css(".close-icon"));
const aDebug = elem.queryAll(By.css("a"));
iconDebug.forEach((item, index) => {
const a: HTMLElement = aDebug[index].nativeElement;
expect(a.textContent).toContain(pageOptions[index]);
五、测试运行
1. 终端操作
(1)如何运行测试代码:命令行输入ng test
注:在test.ts文件夹下配置如下,当执行ng test时,项目目录src下,所有以.spec.ts结尾的文件都会进行测试。
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
(2)如何查看测试结果:命令行输入ng test --code-coverage
注:在Karma.conf.js中配置coverage文件夹路径如下,便可以直接在项目目录下查看测试结果文件夹。
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
或者 angular.json 中配置为 true,配置后每次直接运行 ng test 就会启动代码覆盖率报告。
"test": {
"options": {
"codeCoverage": true
}
}
2. 最终结果
(1)ng test运行项目后,在终端找到地址http://localhost:9876/
(2)将地址复制到浏览器中,打开显示如下(16个用例,0个失败):
(3)打开项目目录coverage文件夹,想要查看哪个组件的代码测试结果,便到组件对应文件夹下,打开index.html即可。例如:no-data.component单元测试结果如下:
可见代码覆盖率分为以下几种指标:语句覆盖率、分支覆盖率、函数覆盖率、行覆盖率。
总结
100%代码覆盖率是一个很难实现的愿景,并且100%覆盖率仅仅意味着所有的代码路径都被执行了,并非已经覆盖了所有的边缘情况,所以应该着眼于测试功能是否全部覆盖,并做代码审查和测试审查。
版权归原作者 dacaicai123 所有, 如有侵权,请联系我们删除。