組件與 Angular 應(yīng)用的所有其它部分不同,它結(jié)合了 HTML 模板和 TypeScript 類。事實(shí)上,組件就是由模板和類一起工作的。要想對組件進(jìn)行充分的測試,你應(yīng)該測試它們是否如預(yù)期般協(xié)同工作。
這些測試需要在瀏覽器 DOM 中創(chuàng)建該組件的宿主元素,就像 Angular 所做的那樣,然后檢查組件類與 DOM 的交互是否如模板中描述的那樣工作。
Angular 的 ?TestBed
?可以幫你做這種測試,正如你將在下面的章節(jié)中看到的那樣。但是,在很多情況下,單獨(dú)測試組件類(不需要 DOM 的參與),就能以更簡單,更明顯的方式驗(yàn)證組件的大部分行為。
如果你要試驗(yàn)本指南中所講的應(yīng)用,請在瀏覽器中運(yùn)行它或下載并在本地運(yùn)行它。
你可以像測試服務(wù)類那樣來測試一個(gè)組件類本身。
組件類的測試應(yīng)該保持非常干凈和簡單。它應(yīng)該只測試一個(gè)單元。一眼看上去,你就應(yīng)該能夠理解正在測試的對象。
考慮這個(gè) ?LightswitchComponent
?,當(dāng)用戶單擊該按鈕時(shí),它會打開和關(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'}`; }
}
你可能要測試 ?clicked()
? 方法是否切換了燈的開/關(guān)狀態(tài)并正確設(shè)置了這個(gè)消息。
這個(gè)組件類沒有依賴。要測試這種類型的組件類,請遵循與沒有依賴的服務(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)聽通過所選?@Output
? 屬性引發(fā)的一個(gè)事件。
你可以測試類代碼的工作方式,而無需創(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
?來同時(shí)創(chuàng)建該組件及其依賴。
下列的 ?WelcomeComponent
?依賴于 ?UserService
?來了解要問候的用戶的名字。
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);
});
然后,測驗(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');
});
測試組件類和測試服務(wù)一樣簡單。
但組件不僅僅是它的類。組件還會與 DOM 以及其他組件進(jìn)行交互。只對類的測試可以告訴你類的行為。但它們無法告訴你這個(gè)組件是否能正確渲染、響應(yīng)用戶輸入和手勢,或是集成到它的父組件和子組件中。
以上所有只對類的測試都不能回答有關(guān)組件會如何在屏幕上實(shí)際運(yùn)行方面的關(guān)鍵問題。
Lightswitch.clicked()
? 綁定到了什么?用戶可以調(diào)用它嗎?
Lightswitch.message
? 是否顯示過?
DashboardHeroComponent
?顯示的英雄?
WelcomeComponent
?的模板是否顯示了歡迎信息?對于上面描述的那些簡單組件來說,這些問題可能并不麻煩。但是很多組件都與模板中描述的 DOM 元素進(jìn)行了復(fù)雜的交互,導(dǎo)致一些 HTML 會在組件狀態(tài)發(fā)生變化時(shí)出現(xiàn)和消失。
要回答這些問題,你必須創(chuàng)建與組件關(guān)聯(lián)的 DOM 元素,你必須檢查 DOM 以確認(rèn)組件狀態(tài)是否在適當(dāng)?shù)臅r(shí)候正確顯示了,并且你必須模擬用戶與屏幕的交互以確定這些交互是否正確。判斷該組件的行為是否符合預(yù)期。
為了編寫這些類型的測試,你將使用 ?TestBed
?的其它特性以及其他的測試輔助函數(shù)。
當(dāng)你要求 CLI 生成一個(gè)新組件時(shí),它會默認(rèn)為你創(chuàng)建一個(gè)初始的測試文件。
比如,下列 CLI 命令會在 ?app/banner
? 文件夾中生成帶有內(nèi)聯(lián)模板和內(nèi)聯(lián)樣式的 ?BannerComponent
?:
ng generate component banner --inline-template --inline-style --module app
它還會生成一個(gè)初始測試文件 ?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è)文件的最后三行才是真正測試組件的,并且所有這些都斷言了 Angular 可以創(chuàng)建該組件。
該文件的其它部分是做設(shè)置用的樣板代碼,可以預(yù)見,如果組件演變得更具實(shí)質(zhì)性內(nèi)容,就會需要更高級的測試。
下面你將學(xué)習(xí)這些高級測試特性?,F(xiàn)在,你可以從根本上把這個(gè)測試文件減少到一個(gè)更容易管理的大?。?/p>
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ù)對象只是聲明了要測試的組件 ?BannerComponent
?。
TestBed.configureTestingModule({declarations: [BannerComponent]});
沒有必要聲明或?qū)肴魏纹渌麞|西。默認(rèn)的測試模塊預(yù)先配置了像來自 ?@angular/platform-browser
? 的 ?BrowserModule
?這樣的東西。
稍后你會用 ?imports
?、?providers
?和更多可聲明對象的參數(shù)來調(diào)用 ?TestBed.configureTestingModule()
?,以滿足你的測試需求??蛇x方法 ?override
?可以進(jìn)一步微調(diào)此配置的各個(gè)方面。
在配置好 ?TestBed
?之后,你就可以調(diào)用它的 ?createComponent()
? 方法了。
const fixture = TestBed.createComponent(BannerComponent);
?TestBed.createComponent()
? 會創(chuàng)建 ?BannerComponent
?的實(shí)例,它把一個(gè)對應(yīng)元素添加到了測試運(yùn)行器的 DOM 中,并返回一個(gè)?ComponentFixture
?對象。
調(diào)用 ?createComponent
?后不能再重新配置 ?TestBed
?。
?createComponent
?方法會凍結(jié)當(dāng)前的 ?TestBed
?定義,并把它關(guān)閉以防止進(jìn)一步的配置。
你不能再調(diào)用任何 ?TestBed
?配置方法,無論是 ?configureTestingModule()
?、?get()
? 還是 ?override...
?方法都不行。如果你這樣做,?TestBed
?會拋出一個(gè)錯誤。
?ComponentFixture
?是一個(gè)測試挽具,用于與所創(chuàng)建的組件及其對應(yīng)的元素進(jìn)行交互。
可以通過測試夾具(fixture)訪問組件實(shí)例,并用 Jasmine 的期望斷言來確認(rèn)它是否存在:
const component = fixture.componentInstance;
expect(component).toBeDefined();
隨著這個(gè)組件的發(fā)展,你會添加更多的測試。你不必為每個(gè)測試復(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è)測試程序,它從 ?fixture.nativeElement
? 中獲取組件的元素,并查找預(yù)期的文本。
it('should contain "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
expect(bannerElement.textContent).toContain('banner works!');
});
?ComponentFixture.nativeElement
? 的值是 ?any
?類型的。稍后你會遇到 ?DebugElement.nativeElement
?,它也是 ?any
?類型的。
Angular 在編譯時(shí)不知道 ?nativeElement
?是什么樣的 HTML 元素,甚至可能不是 HTML 元素。該應(yīng)用可能運(yùn)行在非瀏覽器平臺(如服務(wù)器或 Web Worker)上,在那里本元素可能具有一個(gè)縮小版的 API,甚至根本不存在。
本指南中的測試都是為了在瀏覽器中運(yùn)行而設(shè)計(jì)的,因此 ?nativeElement
?的值始終是 ?HTMLElement
?或其派生類之一。
知道了它是某種 ?HTMLElement
?,就可以用標(biāo)準(zhǔn)的 HTML ?querySelector
?深入了解元素樹。
這是另一個(gè)調(diào)用 ?HTMLElement.querySelector
? 來獲取段落元素并查找橫幅文本的測試:
it('should have <p> with "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
const p = bannerElement.querySelector('p')!;
expect(p.textContent).toEqual('banner works!');
});
Angular 的測試夾具可以直接通過 ?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;
使用這種迂回的路徑訪問元素是有充分理由的。
?nativeElement
?的屬性依賴于其運(yùn)行時(shí)環(huán)境。你可以在非瀏覽器平臺上運(yùn)行這些測試,那些平臺上可能沒有 DOM,或者其模擬的 DOM 不支持完整的 ?HTMLElement
?API。
Angular 依靠 ?DebugElement
?抽象來在其支持的所有平臺上安全地工作。Angular 不會創(chuàng)建 HTML 元素樹,而會創(chuàng)建一個(gè) ?DebugElement
?樹來封裝運(yùn)行時(shí)平臺上的原生元素。?nativeElement
?屬性會解包 ?DebugElement
?并返回特定于平臺的元素對象。
由于本指南的范例測試只能在瀏覽器中運(yùn)行,因此 ?nativeElement
?在這些測試中始終是 ?HTMLElement
?,你可以在測試中探索熟悉的方法和屬性。
下面是把前述測試用 ?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
?還有另一些在測試中很有用的方法和屬性,你可以在本指南的其他地方看到。
你可以從 Angular 的 core 庫中導(dǎo)入 ?DebugElement
?符號。
import { DebugElement } from '@angular/core';
雖然本指南中的測試都是在瀏覽器中運(yùn)行的,但有些應(yīng)用可能至少要在某些時(shí)候運(yùn)行在不同的平臺上。
比如,作為優(yōu)化策略的一部分,該組件可能會首先在服務(wù)器上渲染,以便在連接不良的設(shè)備上更快地啟動本應(yīng)用。服務(wù)器端渲染器可能不支持完整的 HTML 元素 API。如果它不支持 ?querySelector
?,之前的測試就會失敗。
?DebugElement
?提供了適用于其支持的所有平臺的查詢方法。這些查詢方法接受一個(gè)謂詞函數(shù),當(dāng) ?DebugElement
?樹中的一個(gè)節(jié)點(diǎn)與選擇條件匹配時(shí),該函數(shù)返回 ?true
?。
你可以借助從庫中為運(yùn)行時(shí)平臺導(dǎo)入 ?By
? 類來創(chuàng)建一個(gè)謂詞。這里的 ?By
? 是從瀏覽器平臺導(dǎo)入的。
import { By } from '@angular/platform-browser';
下面的例子用 ?DebugElement.query()
? 和瀏覽器的 ?By.css
? 方法重新實(shí)現(xiàn)了前面的測試。
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)行過濾并且只測試瀏覽器原生元素的屬性時(shí),用 ?By.css
? 方法可能會有點(diǎn)過度。
用 ?HTMLElement
?方法(比如 ?querySelector()
? 或 ?querySelectorAll()
?)進(jìn)行過濾通常更簡單,更清晰。
更多建議: