組件與 Angular 應(yīng)用的所有其它部分不同,它結(jié)合了 HTML 模板和 TypeScript 類。事實(shí)上,組件就是由模板和類一起工作的。要想對(duì)組件進(jìn)行充分的測(cè)試,你應(yīng)該測(cè)試它們是否如預(yù)期般協(xié)同工作。
這些測(cè)試需要在瀏覽器 DOM 中創(chuàng)建該組件的宿主元素,就像 Angular 所做的那樣,然后檢查組件類與 DOM 的交互是否如模板中描述的那樣工作。
Angular 的 ?TestBed
?可以幫你做這種測(cè)試,正如你將在下面的章節(jié)中看到的那樣。但是,在很多情況下,單獨(dú)測(cè)試組件類(不需要 DOM 的參與),就能以更簡(jiǎn)單,更明顯的方式驗(yàn)證組件的大部分行為。
如果你要試驗(yàn)本指南中所講的應(yīng)用,請(qǐng)在瀏覽器中運(yùn)行它或下載并在本地運(yùn)行它。
你可以像測(cè)試服務(wù)類那樣來(lái)測(cè)試一個(gè)組件類本身。
組件類的測(cè)試應(yīng)該保持非常干凈和簡(jiǎn)單。它應(yīng)該只測(cè)試一個(gè)單元。一眼看上去,你就應(yīng)該能夠理解正在測(cè)試的對(duì)象。
考慮這個(gè) ?LightswitchComponent
?,當(dāng)用戶單擊該按鈕時(shí),它會(huì)打開(kāi)和關(guān)閉一個(gè)指示燈(用屏幕上的一條消息表示)。
@Component({
selector: 'lightswitch-comp',
template: `
<button type="button" (click)="clicked()">Click me!</button>
<span>{{message}}</span>`
})
export class LightswitchComponent {
isOn = false;
clicked() { this.isOn = !this.isOn; }
get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}
你可能要測(cè)試 ?clicked()
? 方法是否切換了燈的開(kāi)/關(guān)狀態(tài)并正確設(shè)置了這個(gè)消息。
這個(gè)組件類沒(méi)有依賴。要測(cè)試這種類型的組件類,請(qǐng)遵循與沒(méi)有依賴的服務(wù)相同的步驟:
describe('LightswitchComp', () => {
it('#clicked() should toggle #isOn', () => {
const comp = new LightswitchComponent();
expect(comp.isOn)
.withContext('off at first')
.toBe(false);
comp.clicked();
expect(comp.isOn)
.withContext('on after click')
.toBe(true);
comp.clicked();
expect(comp.isOn)
.withContext('off after second click')
.toBe(false);
});
it('#clicked() should set #message to "is on"', () => {
const comp = new LightswitchComponent();
expect(comp.message)
.withContext('off at first')
.toMatch(/is off/i);
comp.clicked();
expect(comp.message)
.withContext('on after clicked')
.toMatch(/is on/i);
});
});
下面是“英雄之旅”教程中的 ?DashboardHeroComponent
?。
export class DashboardHeroComponent {
@Input() hero!: Hero;
@Output() selected = new EventEmitter<Hero>();
click() { this.selected.emit(this.hero); }
}
它出現(xiàn)在父組件的模板中,把一個(gè)英雄綁定到了 ?@Input
? 屬性,并監(jiān)聽(tīng)通過(guò)所選?@Output
? 屬性引發(fā)的一個(gè)事件。
你可以測(cè)試類代碼的工作方式,而無(wú)需創(chuàng)建 ?DashboardHeroComponent
?或它的父組件。
it('raises the selected event when clicked', () => {
const comp = new DashboardHeroComponent();
const hero: Hero = {id: 42, name: 'Test'};
comp.hero = hero;
comp.selected.pipe(first()).subscribe((selectedHero: Hero) => expect(selectedHero).toBe(hero));
comp.click();
});
當(dāng)組件有依賴時(shí),你可能要使用 ?TestBed
?來(lái)同時(shí)創(chuàng)建該組件及其依賴。
下列的 ?WelcomeComponent
?依賴于 ?UserService
?來(lái)了解要問(wèn)候的用戶的名字。
export class WelcomeComponent implements OnInit {
welcome = '';
constructor(private userService: UserService) { }
ngOnInit(): void {
this.welcome = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name : 'Please log in.';
}
}
你可以先創(chuàng)建一個(gè)能滿足本組件最低需求的 ?UserService
?。
class MockUserService {
isLoggedIn = true;
user = { name: 'Test User'};
}
然后在 ?TestBed
?配置中提供并注入所有這些組件和服務(wù)。
beforeEach(() => {
TestBed.configureTestingModule({
// provide the component-under-test and dependent service
providers: [
WelcomeComponent,
{ provide: UserService, useClass: MockUserService }
]
});
// inject both the component and the dependent service.
comp = TestBed.inject(WelcomeComponent);
userService = TestBed.inject(UserService);
});
然后,測(cè)驗(yàn)組件類,別忘了要像 Angular 運(yùn)行應(yīng)用時(shí)一樣調(diào)用生命周期鉤子方法。
it('should not have welcome message after construction', () => {
expect(comp.welcome).toBe('');
});
it('should welcome logged in user after Angular calls ngOnInit', () => {
comp.ngOnInit();
expect(comp.welcome).toContain(userService.user.name);
});
it('should ask user to log in if not logged in after ngOnInit', () => {
userService.isLoggedIn = false;
comp.ngOnInit();
expect(comp.welcome).not.toContain(userService.user.name);
expect(comp.welcome).toContain('log in');
});
測(cè)試組件類和測(cè)試服務(wù)一樣簡(jiǎn)單。
但組件不僅僅是它的類。組件還會(huì)與 DOM 以及其他組件進(jìn)行交互。只對(duì)類的測(cè)試可以告訴你類的行為。但它們無(wú)法告訴你這個(gè)組件是否能正確渲染、響應(yīng)用戶輸入和手勢(shì),或是集成到它的父組件和子組件中。
以上所有只對(duì)類的測(cè)試都不能回答有關(guān)組件會(huì)如何在屏幕上實(shí)際運(yùn)行方面的關(guān)鍵問(wèn)題。
Lightswitch.clicked()
? 綁定到了什么?用戶可以調(diào)用它嗎?
Lightswitch.message
? 是否顯示過(guò)?
DashboardHeroComponent
?顯示的英雄?
WelcomeComponent
?的模板是否顯示了歡迎信息?對(duì)于上面描述的那些簡(jiǎn)單組件來(lái)說(shuō),這些問(wèn)題可能并不麻煩。但是很多組件都與模板中描述的 DOM 元素進(jìn)行了復(fù)雜的交互,導(dǎo)致一些 HTML 會(huì)在組件狀態(tài)發(fā)生變化時(shí)出現(xiàn)和消失。
要回答這些問(wèn)題,你必須創(chuàng)建與組件關(guān)聯(lián)的 DOM 元素,你必須檢查 DOM 以確認(rèn)組件狀態(tài)是否在適當(dāng)?shù)臅r(shí)候正確顯示了,并且你必須模擬用戶與屏幕的交互以確定這些交互是否正確。判斷該組件的行為是否符合預(yù)期。
為了編寫這些類型的測(cè)試,你將使用 ?TestBed
?的其它特性以及其他的測(cè)試輔助函數(shù)。
當(dāng)你要求 CLI 生成一個(gè)新組件時(shí),它會(huì)默認(rèn)為你創(chuàng)建一個(gè)初始的測(cè)試文件。
比如,下列 CLI 命令會(huì)在 ?app/banner
? 文件夾中生成帶有內(nèi)聯(lián)模板和內(nèi)聯(lián)樣式的 ?BannerComponent
?:
ng generate component banner --inline-template --inline-style --module app
它還會(huì)生成一個(gè)初始測(cè)試文件 ?banner-external.component.spec.ts
?,如下所示:
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { BannerComponent } from './banner.component';
describe('BannerComponent', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({declarations: [BannerComponent]}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeDefined();
});
});
由于 ?
compileComponents
?是異步的,所以它使用從 ?@angular/core/testing
? 中導(dǎo)入的實(shí)用工具函數(shù) ?waitForAsync
?。
只有這個(gè)文件的最后三行才是真正測(cè)試組件的,并且所有這些都斷言了 Angular 可以創(chuàng)建該組件。
該文件的其它部分是做設(shè)置用的樣板代碼,可以預(yù)見(jiàn),如果組件演變得更具實(shí)質(zhì)性內(nèi)容,就會(huì)需要更高級(jí)的測(cè)試。
下面你將學(xué)習(xí)這些高級(jí)測(cè)試特性?,F(xiàn)在,你可以從根本上把這個(gè)測(cè)試文件減少到一個(gè)更容易管理的大小:
describe('BannerComponent (minimal)', () => {
it('should create', () => {
TestBed.configureTestingModule({declarations: [BannerComponent]});
const fixture = TestBed.createComponent(BannerComponent);
const component = fixture.componentInstance;
expect(component).toBeDefined();
});
});
在這個(gè)例子中,傳給 ?TestBed.configureTestingModule
? 的元數(shù)據(jù)對(duì)象只是聲明了要測(cè)試的組件 ?BannerComponent
?。
TestBed.configureTestingModule({declarations: [BannerComponent]});
沒(méi)有必要聲明或?qū)肴魏纹渌麞|西。默認(rèn)的測(cè)試模塊預(yù)先配置了像來(lái)自 ?@angular/platform-browser
? 的 ?BrowserModule
?這樣的東西。
稍后你會(huì)用 ?imports
?、?providers
?和更多可聲明對(duì)象的參數(shù)來(lái)調(diào)用 ?TestBed.configureTestingModule()
?,以滿足你的測(cè)試需求??蛇x方法 ?override
?可以進(jìn)一步微調(diào)此配置的各個(gè)方面。
在配置好 ?TestBed
?之后,你就可以調(diào)用它的 ?createComponent()
? 方法了。
const fixture = TestBed.createComponent(BannerComponent);
?TestBed.createComponent()
? 會(huì)創(chuàng)建 ?BannerComponent
?的實(shí)例,它把一個(gè)對(duì)應(yīng)元素添加到了測(cè)試運(yùn)行器的 DOM 中,并返回一個(gè)?ComponentFixture
?對(duì)象。
調(diào)用 ?createComponent
?后不能再重新配置 ?TestBed
?。
?createComponent
?方法會(huì)凍結(jié)當(dāng)前的 ?TestBed
?定義,并把它關(guān)閉以防止進(jìn)一步的配置。
你不能再調(diào)用任何 ?TestBed
?配置方法,無(wú)論是 ?configureTestingModule()
?、?get()
? 還是 ?override...
?方法都不行。如果你這樣做,?TestBed
?會(huì)拋出一個(gè)錯(cuò)誤。
?ComponentFixture
?是一個(gè)測(cè)試挽具,用于與所創(chuàng)建的組件及其對(duì)應(yīng)的元素進(jìn)行交互。
可以通過(guò)測(cè)試夾具(fixture)訪問(wèn)組件實(shí)例,并用 Jasmine 的期望斷言來(lái)確認(rèn)它是否存在:
const component = fixture.componentInstance;
expect(component).toBeDefined();
隨著這個(gè)組件的發(fā)展,你會(huì)添加更多的測(cè)試。你不必為每個(gè)測(cè)試復(fù)制 ?TestBed
?的配置代碼,而是把它重構(gòu)到 Jasmine 的 ?beforeEach()
? 和一些支持變量中:
describe('BannerComponent (with beforeEach)', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({declarations: [BannerComponent]});
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeDefined();
});
});
現(xiàn)在添加一個(gè)測(cè)試程序,它從 ?fixture.nativeElement
? 中獲取組件的元素,并查找預(yù)期的文本。
it('should contain "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
expect(bannerElement.textContent).toContain('banner works!');
});
?ComponentFixture.nativeElement
? 的值是 ?any
?類型的。稍后你會(huì)遇到 ?DebugElement.nativeElement
?,它也是 ?any
?類型的。
Angular 在編譯時(shí)不知道 ?nativeElement
?是什么樣的 HTML 元素,甚至可能不是 HTML 元素。該應(yīng)用可能運(yùn)行在非瀏覽器平臺(tái)(如服務(wù)器或 Web Worker)上,在那里本元素可能具有一個(gè)縮小版的 API,甚至根本不存在。
本指南中的測(cè)試都是為了在瀏覽器中運(yùn)行而設(shè)計(jì)的,因此 ?nativeElement
?的值始終是 ?HTMLElement
?或其派生類之一。
知道了它是某種 ?HTMLElement
?,就可以用標(biāo)準(zhǔn)的 HTML ?querySelector
?深入了解元素樹(shù)。
這是另一個(gè)調(diào)用 ?HTMLElement.querySelector
? 來(lái)獲取段落元素并查找橫幅文本的測(cè)試:
it('should have <p> with "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
const p = bannerElement.querySelector('p')!;
expect(p.textContent).toEqual('banner works!');
});
Angular 的測(cè)試夾具可以直接通過(guò) ?fixture.nativeElement
? 提供組件的元素。
const bannerElement: HTMLElement = fixture.nativeElement;
它實(shí)際上是一個(gè)便利方法,其最終實(shí)現(xiàn)為 ?fixture.debugElement.nativeElement
?。
const bannerDe: DebugElement = fixture.debugElement;
const bannerEl: HTMLElement = bannerDe.nativeElement;
使用這種迂回的路徑訪問(wèn)元素是有充分理由的。
?nativeElement
?的屬性依賴于其運(yùn)行時(shí)環(huán)境。你可以在非瀏覽器平臺(tái)上運(yùn)行這些測(cè)試,那些平臺(tái)上可能沒(méi)有 DOM,或者其模擬的 DOM 不支持完整的 ?HTMLElement
?API。
Angular 依靠 ?DebugElement
?抽象來(lái)在其支持的所有平臺(tái)上安全地工作。Angular 不會(huì)創(chuàng)建 HTML 元素樹(shù),而會(huì)創(chuàng)建一個(gè) ?DebugElement
?樹(shù)來(lái)封裝運(yùn)行時(shí)平臺(tái)上的原生元素。?nativeElement
?屬性會(huì)解包 ?DebugElement
?并返回特定于平臺(tái)的元素對(duì)象。
由于本指南的范例測(cè)試只能在瀏覽器中運(yùn)行,因此 ?nativeElement
?在這些測(cè)試中始終是 ?HTMLElement
?,你可以在測(cè)試中探索熟悉的方法和屬性。
下面是把前述測(cè)試用 ?fixture.debugElement.nativeElement
? 重新實(shí)現(xiàn)的版本:
it('should find the <p> with fixture.debugElement.nativeElement)', () => {
const bannerDe: DebugElement = fixture.debugElement;
const bannerEl: HTMLElement = bannerDe.nativeElement;
const p = bannerEl.querySelector('p')!;
expect(p.textContent).toEqual('banner works!');
});
這些 ?DebugElement
?還有另一些在測(cè)試中很有用的方法和屬性,你可以在本指南的其他地方看到。
你可以從 Angular 的 core 庫(kù)中導(dǎo)入 ?DebugElement
?符號(hào)。
import { DebugElement } from '@angular/core';
雖然本指南中的測(cè)試都是在瀏覽器中運(yùn)行的,但有些應(yīng)用可能至少要在某些時(shí)候運(yùn)行在不同的平臺(tái)上。
比如,作為優(yōu)化策略的一部分,該組件可能會(huì)首先在服務(wù)器上渲染,以便在連接不良的設(shè)備上更快地啟動(dòng)本應(yīng)用。服務(wù)器端渲染器可能不支持完整的 HTML 元素 API。如果它不支持 ?querySelector
?,之前的測(cè)試就會(huì)失敗。
?DebugElement
?提供了適用于其支持的所有平臺(tái)的查詢方法。這些查詢方法接受一個(gè)謂詞函數(shù),當(dāng) ?DebugElement
?樹(shù)中的一個(gè)節(jié)點(diǎn)與選擇條件匹配時(shí),該函數(shù)返回 ?true
?。
你可以借助從庫(kù)中為運(yùn)行時(shí)平臺(tái)導(dǎo)入 ?By
? 類來(lái)創(chuàng)建一個(gè)謂詞。這里的 ?By
? 是從瀏覽器平臺(tái)導(dǎo)入的。
import { By } from '@angular/platform-browser';
下面的例子用 ?DebugElement.query()
? 和瀏覽器的 ?By.css
? 方法重新實(shí)現(xiàn)了前面的測(cè)試。
it('should find the <p> with fixture.debugElement.query(By.css)', () => {
const bannerDe: DebugElement = fixture.debugElement;
const paragraphDe = bannerDe.query(By.css('p'));
const p: HTMLElement = paragraphDe.nativeElement;
expect(p.textContent).toEqual('banner works!');
});
一些值得注意的地方:
By.css()
? 靜態(tài)方法使用標(biāo)準(zhǔn) CSS 選擇器選擇 ?DebugElement
?節(jié)點(diǎn)。
DebugElement
?。
當(dāng)你使用 CSS 選擇器進(jìn)行過(guò)濾并且只測(cè)試瀏覽器原生元素的屬性時(shí),用 ?By.css
? 方法可能會(huì)有點(diǎn)過(guò)度。
用 ?HTMLElement
?方法(比如 ?querySelector()
? 或 ?querySelectorAll()
?)進(jìn)行過(guò)濾通常更簡(jiǎn)單,更清晰。
更多建議: