Angular
项目创建时,会自动生成spec.ts
单元测试文件. 在ionic
项目中, 需要手动创建spec.ts
文件和添加依赖的框架.
添加配置依赖可以参看ionic-team
写的一篇文章 :点我😄
单元测试基础入门
简单示例
describe('A suite is just function',()=>{
var a;
it('and so is a spec ', function () {
a = true;
expect(a).toBe(true);
});
})
执行结果:
上面的例子是一个很简单的单元测试, 它有三部分组成: describe()
, it()
和 expect()
组成;
编写单元测试用例的基本步骤:
- 设计测试用例函数: 根据测试场景组合使用
describe()
和it()
来定义测试用例; - 编写判断逻辑: 在
it()
函数中, 需要使用expect()
断言函数来判定用例测试结果. 例如,expect(a).toBe(true);
表示变量a
应等于true
,否则说明这个用例测试失败.
测试集
在Jasmine
中, 将功能相似的测试用例统一聚集在测试集(Suites)
中, 并通过describe()
函数标识. 假设被测试的业务类有多个函数, 则需要编写多个测试用例, 并将这些测试用例都放在同个 describe()
函数中.
测试点
每个具体的功能测试点 (Spec)
可以用全局函数it()
来定义, 该函数的第一个参数表示该用例的名称, 第二个参数为测试的细节. it()
函数可以包含一个或者多个断言 (expect)
, 每个断言的结果只能是 true
或者false
, 只有当所有断言都为true
是, 该测试点才算通过.
内置匹配器
内置匹配器(Matchers)
, 就是更在expect()
函数后面, 用于判断结果是否符合期望值的函数. 在上面的例子中, 已经使用了一个很常用的匹配器toBe()
.
内置常用匹配器
- toBe()
toBe()
本质是使用操作符 ===
来比较结果值和期望值, not.toBe()
则表示不等, 示例代码如下:
it("The 'toBe()' matcher compares with '===' ", function () {
var a = 15;
var b = a;
expect(a).toBe(b);
expect(a).not.toBe(null);
});
- toEqual()
toEqual()
用于比较两个对象、数字、字符串等, 示例代码如下:
it('should work for objects', function () {
var foo = {
a: 8,
b: 15
};
var bar = {
a: 8,
b: 15
}
expect(foo).toEqual(bar);
});
- toMatch()
toMatch()
用于验证是否匹配正则表达式, 示例代码如下:
it("The 'toMatch' matcher is for regular expressions", function () {
var message = 'Ionic CLI and Cordova';
expect(message).toMatch(/ionic/i);
});
- toContain()
toContain()
用于验证数组是否包含指定元素, 示例代码如下:
it("The 'toContain' matcher is for finding an item in an array", function () {
var array = ['React', 'Vue', 'Angular'];
expect(array).toContain('Angular');
});
Jasmine 内置匹配器:
匹配器 | 作用 |
---|---|
toBe |
使用 === 比较结果 |
toEqual |
比较两个对象、数字、字符串等是否相等 |
toMatch |
正则表达式匹配 |
toBeNull |
验证是否为null |
toBeTruthy |
验证是否为 true |
toBeFalsy |
验证是否为 false |
toBeLessThan |
验证结果是否小于指定值 |
toBeGreaterThan |
验证结果是否大于指定值 |
toContain |
验证数组是否包含指定元素 |
toBeCloseTo |
将值进行四舍五入后比较是否相等 |
toThrow |
验证函数是否抛出一个错误 |
toThrowError |
验证函数是否抛出指定的错误 |
toBeDefined |
验证对象是否 不为 undefined
|
beforeEach 、afterEach函数
另外, 在测试中还会经常用到 beforeEach()
、afterEach()
这两个函数, 用于定义每个测试用例执行前及执行后的公共逻辑 。
-
beforeEach()
: 定义了每个用例在执行前, 所需要执行的初始化函数。 -
afterEach ()
: 每个用例在执行结束后, 均将执行afterEach ()
函数。
beforeEach()
的简单使用:
describe('How to use beforeEach', () => {
let foo, bar;
beforeEach(() => {
foo = {
a: 8,
b: 15
};
bar = {
a: 8,
b: 15
}
})
it('should work for objects', function () {
expect(foo).toEqual(bar);
});
})
单元测试
-
组件的测试
page3.html
:<ion-content no-padding> <ion-list> <button ion-item *ngFor="let item of list"> {{item}} </button> </ion-list> </ion-content>
page3.ts
:export class Page3 { list: string[] = ['张三', '李四', '王五']; constructor() {} ionViewDidLoad() {} addItem() { this.list.push(`赵六`); } }
page3.spec.ts
import {Page3} from "./page3"; describe('Page3 组件', () => { let component; beforeEach(() => { component = new Page3(); }); it("调用 addItem() 函数, 并检测该函数是否添加了 '赵六'", function() { component.addItem(); expect(component.list).toContain('赵六'); }); })
-
管道的测试
要求: 创建一个pipe
, 把手机号(15912345678
)显示为159-1234-5678
的形式,并对该pipe
做单元测试。
ll-phone.ts
import {Pipe, PipeTransform} from '@angular/core'; @Pipe({ name: 'llPhone', }) export class LlPhonePipe implements PipeTransform { transform(value: string): string { if (!value) return ''; if (value.length === 11) { return value.replace(/(\d{3})(\d{4})(\d{4})/, (m, m1, m2, m3) => { return [m1, m2, m3].join('-'); }) } else { return value; } } }
ll-phone.spec.ts
:import {LlPhonePipe} from "./ll-phone"; describe('测试 llPhonePipe', () => { let phonePipe: LlPhonePipe; beforeEach(() => { phonePipe = new LlPhonePipe(); }); it('should transforms phone', function () { expect(phonePipe.transform('15912345678')).toEqual('159-1234-5678'); }); })
-
服务的测试
创建一个MockData
的LlMockDataProvider
ll-mock-data.ts
:import {Injectable} from '@angular/core'; @Injectable() export class LlMockDataProvider { constructor() {} public hotMenu = [{ "img": "http://file.j1home.com/%E5%AE%89%E5%85%A8%E7%A4%BE%E5%8C%BA@2x.png", "name": "社区安全" }, { "img": "http://file.j1home.com/%E7%A4%BE%E5%8C%BA%E5%85%9A%E5%BB%BA@2x.png", "name": "社区党建" }]; }
ll-mock-data.spec.ts
:import {inject, TestBed} from "@angular/core/testing"; import {LlMockDataProvider} from "./ll-mock-data"; describe('test provider', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [LlMockDataProvider] }); }); it('should contain item', inject([LlMockDataProvider], (service) => { expect(service.hotMenu[1]['name']).toEqual('社区党建'); })); })
通过TestBed
注入被测类
在上面的代码ll-mock-data.spec.ts
中,我们使用到了TestBed.configureTestingModule()
,它为我们创建了一个测试 NgModule
,在它的元数据里面,我们可以注入被测试的类。如果有依赖,比如:Http
,那么你需要 imports: [HttpModule]
,这一切都跟 NgModule
写法完全一置。
beforeEach(() => {
TestBed.configureTestingModule({
providers: [LlMockDataProvider]
});
});
组件 DOM 交互测试
在下面的图片中, 如果想检测DOM
元素的内容是否符合我们的要求,
可通过TestBed
进行组件的初始化, 可以获取组件模板对应的DOM
元素以及检查 DOM
元素的内容等。
示例代码如下:
page3.html
<ion-content no-padding>
<ion-list>
<button ion-item *ngFor="let item of list">
{{item | llPhone}}
</button>
</ion-list>
</ion-content>
page3.ts
import {Component} from '@angular/core';
import {IonicPage, NavController, NavParams} from 'ionic-angular';
@IonicPage()
@Component({
selector: 'page-page3',
templateUrl: 'page3.html',
})
export class Page3 {
list: string[] = ['15512345678'];
constructor() {}
ionViewDidLoad() {}
addItem() {
this.list.push(`15512345678`);
}
}
page3.spec.ts
import {TestBed} from "@angular/core/testing";
import {Page3} from "./page3";
import {IonicModule} from "ionic-angular";
import {LlPhonePipe} from "../../pipes/ll-phone/ll-phone";
describe('Page3 组件', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Page3, LlPhonePipe],
imports: [
IonicModule.forRoot(Page3)
],
});
});
it('test simple component with TestComponentBuilder', () => {
var fixture = TestBed.createComponent(Page3);
var compiled = fixture.debugElement.nativeElement;
// 调用 组件的 addItem() 函数
fixture.componentInstance.addItem();
// 变化监测
fixture.detectChanges();
// replace(/(^\s*)|(\s*$)/g, "") 移除字符串首尾空格
let val = compiled.querySelector('.label').textContent.replace(/(^\s*)|(\s*$)/g, "");
expect(val).toBe('155-1234-5678');
});
});
在上面的代码中, 需要关注以下的知识点:
-
TestBed
的createComponent()
方法用于初始化被测组件, 并创建ComponentFixture
对象, 这个对象可以认为是被测组件的上下文环境, 通过它可以获取已经完成初始化的组件实例以及DOM
元素等。 -
fixture.detectChanges()
方法用于当JavaScript
变量及模板内容变更时触发变化监测。组件初始化、DOM
元素或JavaScript
值变化时,都需要调用这个方法, 以便触发Angular
的变化监测机制。 -
fixture.debugElement.nativeElement
用于获取组件对应的原生DOM
元素, 之后可以通过querySelector()
等DOM
元素原生API
做进一步处理。 -
fixture.componentInstance
用于获取组件对应的对象实例,上面代码中通过fixture.componentInstance
调用该实例对象的addItem()
函数:fixture.componentInstance.addItem();
。