Angular項(xiàng)目創(chuàng)建時(shí),會(huì)自動(dòng)生成spec.ts單元測試文件. 在ionic項(xiàng)目中, 需要手動(dòng)創(chuàng)建spec.ts文件和添加依賴的框架.
添加配置依賴可以參看ionic-team寫的一篇文章 :點(diǎn)我??
單元測試基礎(chǔ)入門
簡單示例
describe('A suite is just function',()=>{
var a;
it('and so is a spec ', function () {
a = true;
expect(a).toBe(true);
});
})
執(zhí)行結(jié)果:

上面的例子是一個(gè)很簡單的單元測試, 它有三部分組成: describe() , it() 和 expect()組成;
編寫單元測試用例的基本步驟:
- 設(shè)計(jì)測試用例函數(shù): 根據(jù)測試場景組合使用
describe()和it()來定義測試用例; - 編寫判斷邏輯: 在
it()函數(shù)中, 需要使用expect()斷言函數(shù)來判定用例測試結(jié)果. 例如,expect(a).toBe(true);表示變量a應(yīng)等于true,否則說明這個(gè)用例測試失敗.
a 等于 false, 則用例測試失敗
測試集
在Jasmine中, 將功能相似的測試用例統(tǒng)一聚集在測試集(Suites)中, 并通過describe()函數(shù)標(biāo)識(shí). 假設(shè)被測試的業(yè)務(wù)類有多個(gè)函數(shù), 則需要編寫多個(gè)測試用例, 并將這些測試用例都放在同個(gè) describe()函數(shù)中.
測試點(diǎn)
每個(gè)具體的功能測試點(diǎn) (Spec) 可以用全局函數(shù)it()來定義, 該函數(shù)的第一個(gè)參數(shù)表示該用例的名稱, 第二個(gè)參數(shù)為測試的細(xì)節(jié). it()函數(shù)可以包含一個(gè)或者多個(gè)斷言 (expect), 每個(gè)斷言的結(jié)果只能是 true或者false, 只有當(dāng)所有斷言都為true是, 該測試點(diǎn)才算通過.
內(nèi)置匹配器
內(nèi)置匹配器(Matchers), 就是更在expect()函數(shù)后面, 用于判斷結(jié)果是否符合期望值的函數(shù). 在上面的例子中, 已經(jīng)使用了一個(gè)很常用的匹配器toBe().
內(nèi)置常用匹配器
- toBe()
toBe()本質(zhì)是使用操作符 ===來比較結(jié)果值和期望值, 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()用于比較兩個(gè)對象、數(shù)字、字符串等, 示例代碼如下:
it('should work for objects', function () {
var foo = {
a: 8,
b: 15
};
var bar = {
a: 8,
b: 15
}
expect(foo).toEqual(bar);
});
- toMatch()
toMatch()用于驗(yàn)證是否匹配正則表達(dá)式, 示例代碼如下:
it("The 'toMatch' matcher is for regular expressions", function () {
var message = 'Ionic CLI and Cordova';
expect(message).toMatch(/ionic/i);
});
- toContain()
toContain()用于驗(yàn)證數(shù)組是否包含指定元素, 示例代碼如下:
it("The 'toContain' matcher is for finding an item in an array", function () {
var array = ['React', 'Vue', 'Angular'];
expect(array).toContain('Angular');
});
Jasmine 內(nèi)置匹配器:
| 匹配器 | 作用 |
|---|---|
toBe |
使用 === 比較結(jié)果 |
toEqual |
比較兩個(gè)對象、數(shù)字、字符串等是否相等 |
toMatch |
正則表達(dá)式匹配 |
toBeNull |
驗(yàn)證是否為null |
toBeTruthy |
驗(yàn)證是否為 true |
toBeFalsy |
驗(yàn)證是否為 false |
toBeLessThan |
驗(yàn)證結(jié)果是否小于指定值 |
toBeGreaterThan |
驗(yàn)證結(jié)果是否大于指定值 |
toContain |
驗(yàn)證數(shù)組是否包含指定元素 |
toBeCloseTo |
將值進(jìn)行四舍五入后比較是否相等 |
toThrow |
驗(yàn)證函數(shù)是否拋出一個(gè)錯(cuò)誤 |
toThrowError |
驗(yàn)證函數(shù)是否拋出指定的錯(cuò)誤 |
toBeDefined |
驗(yàn)證對象是否 不為 undefined
|
beforeEach 、afterEach函數(shù)
另外, 在測試中還會(huì)經(jīng)常用到 beforeEach() 、afterEach() 這兩個(gè)函數(shù), 用于定義每個(gè)測試用例執(zhí)行前及執(zhí)行后的公共邏輯 。
-
beforeEach(): 定義了每個(gè)用例在執(zhí)行前, 所需要執(zhí)行的初始化函數(shù)。 -
afterEach (): 每個(gè)用例在執(zhí)行結(jié)束后, 均將執(zhí)行afterEach ()函數(shù)。
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.tsimport {Page3} from "./page3"; describe('Page3 組件', () => { let component; beforeEach(() => { component = new Page3(); }); it("調(diào)用 addItem() 函數(shù), 并檢測該函數(shù)是否添加了 '趙六'", function() { component.addItem(); expect(component.list).toContain('趙六'); }); })

-
管道的測試
要求: 創(chuàng)建一個(gè)pipe, 把手機(jī)號(hào)(15912345678)顯示為159-1234-5678的形式,并對該pipe做單元測試。
ll-phone.tsimport {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'); }); }) -
服務(wù)的測試
創(chuàng)建一個(gè)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": "社區(qū)安全" }, { "img": "http://file.j1home.com/%E7%A4%BE%E5%8C%BA%E5%85%9A%E5%BB%BA@2x.png", "name": "社區(qū)黨建" }]; }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('社區(qū)黨建'); })); })
通過TestBed注入被測類
在上面的代碼ll-mock-data.spec.ts中,我們使用到了TestBed.configureTestingModule(),它為我們創(chuàng)建了一個(gè)測試 NgModule,在它的元數(shù)據(jù)里面,我們可以注入被測試的類。如果有依賴,比如:Http,那么你需要 imports: [HttpModule],這一切都跟 NgModule 寫法完全一置。
beforeEach(() => {
TestBed.configureTestingModule({
providers: [LlMockDataProvider]
});
});
組件 DOM 交互測試
在下面的圖片中, 如果想檢測DOM元素的內(nèi)容是否符合我們的要求,
可通過TestBed進(jìn)行組件的初始化, 可以獲取組件模板對應(yīng)的DOM元素以及檢查 DOM元素的內(nèi)容等。

示例代碼如下:
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;
// 調(diào)用 組件的 addItem() 函數(shù)
fixture.componentInstance.addItem();
// 變化監(jiān)測
fixture.detectChanges();
// replace(/(^\s*)|(\s*$)/g, "") 移除字符串首尾空格
let val = compiled.querySelector('.label').textContent.replace(/(^\s*)|(\s*$)/g, "");
expect(val).toBe('155-1234-5678');
});
});
在上面的代碼中, 需要關(guān)注以下的知識(shí)點(diǎn):
-
TestBed的createComponent()方法用于初始化被測組件, 并創(chuàng)建ComponentFixture對象, 這個(gè)對象可以認(rèn)為是被測組件的上下文環(huán)境, 通過它可以獲取已經(jīng)完成初始化的組件實(shí)例以及DOM元素等。 -
fixture.detectChanges()方法用于當(dāng)JavaScript變量及模板內(nèi)容變更時(shí)觸發(fā)變化監(jiān)測。組件初始化、DOM元素或JavaScript值變化時(shí),都需要調(diào)用這個(gè)方法, 以便觸發(fā)Angular的變化監(jiān)測機(jī)制。 -
fixture.debugElement.nativeElement用于獲取組件對應(yīng)的原生DOM元素, 之后可以通過querySelector()等DOM元素原生API做進(jìn)一步處理。 -
fixture.componentInstance用于獲取組件對應(yīng)的對象實(shí)例,上面代碼中通過fixture.componentInstance調(diào)用該實(shí)例對象的addItem()函數(shù):fixture.componentInstance.addItem();。
