掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
組件與 Angular 應(yīng)用的所有其它部分不同,它結(jié)合了 HTML 模板和 TypeScript 類。事實(shí)上,組件就是由模板和類一起工作的。要想對(duì)組件進(jìn)行充分的測(cè)試,你應(yīng)該測(cè)試它們是否如預(yù)期般協(xié)同工作。

欽州網(wǎng)站建設(shè)公司成都創(chuàng)新互聯(lián)公司,欽州網(wǎng)站設(shè)計(jì)制作,有大型網(wǎng)站制作公司豐富經(jīng)驗(yàn)。已為欽州1000+提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\成都外貿(mào)網(wǎng)站建設(shè)公司要多少錢,請(qǐng)找那個(gè)售后服務(wù)好的欽州做網(wǎng)站的公司定做!
這些測(cè)試需要在瀏覽器 DOM 中創(chuàng)建該組件的宿主元素,就像 Angular 所做的那樣,然后檢查組件類與 DOM 的交互是否如模板中描述的那樣工作。
Angular 的 ?TestBed ?可以幫你做這種測(cè)試,正如你將在下面的章節(jié)中看到的那樣。但是,在很多情況下,單獨(dú)測(cè)試組件類(不需要 DOM 的參與),就能以更簡(jiǎn)單,更明顯的方式驗(yàn)證組件的大部分行為。
如果你要試驗(yàn)本指南中所講的應(yīng)用,請(qǐng)?jiān)跒g覽器中運(yùn)行它或下載并在本地運(yùn)行它。
你可以像測(cè)試服務(wù)類那樣來測(cè)試一個(gè)組件類本身。
組件類的測(cè)試應(yīng)該保持非常干凈和簡(jiǎn)單。它應(yīng)該只測(cè)試一個(gè)單元。一眼看上去,你就應(yīng)該能夠理解正在測(cè)試的對(duì)象。
考慮這個(gè) ?LightswitchComponent?,當(dāng)用戶單擊該按鈕時(shí),它會(huì)打開和關(guān)閉一個(gè)指示燈(用屏幕上的一條消息表示)。
@Component({
selector: 'lightswitch-comp',
template: `
{{message}}`
})
export class LightswitchComponent {
isOn = false;
clicked() { this.isOn = !this.isOn; }
get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}你可能要測(cè)試 ?clicked()? 方法是否切換了燈的開/關(guān)狀態(tài)并正確設(shè)置了這個(gè)消息。
這個(gè)組件類沒有依賴。要測(cè)試這種類型的組件類,請(qǐng)遵循與沒有依賴的服務(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();
click() { this.selected.emit(this.hero); }
} 它出現(xiàn)在父組件的模板中,把一個(gè)英雄綁定到了 ?@Input? 屬性,并監(jiān)聽通過所選?@Output? 屬性引發(fā)的一個(gè)事件。
你可以測(cè)試類代碼的工作方式,而無需創(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);
});然后,測(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è)試可以告訴你類的行為。但它們無法告訴你這個(gè)組件是否能正確渲染、響應(yīng)用戶輸入和手勢(shì),或是集成到它的父組件和子組件中。
以上所有只對(duì)類的測(cè)試都不能回答有關(guān)組件會(huì)如何在屏幕上實(shí)際運(yùn)行方面的關(guān)鍵問題。
Lightswitch.clicked()? 綁定到了什么?用戶可以調(diào)用它嗎? Lightswitch.message? 是否顯示過? DashboardHeroComponent ?顯示的英雄? WelcomeComponent ?的模板是否顯示了歡迎信息?對(duì)于上面描述的那些簡(jiǎn)單組件來說,這些問題可能并不麻煩。但是很多組件都與模板中描述的 DOM 元素進(jìn)行了復(fù)雜的交互,導(dǎo)致一些 HTML 會(huì)在組件狀態(tài)發(fā)生變化時(shí)出現(xià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;
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ù)見,如果組件演變得更具實(shí)質(zhì)性內(nèi)容,就會(huì)需要更高級(jí)的測(cè)試。
下面你將學(xué)習(xí)這些高級(jí)測(cè)試特性?,F(xiàn)在,你可以從根本上把這個(gè)測(cè)試文件減少到一個(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ù)對(duì)象只是聲明了要測(cè)試的組件 ?BannerComponent?。
TestBed.configureTestingModule({declarations: [BannerComponent]});
沒有必要聲明或?qū)肴魏纹渌麞|西。默認(rèn)的測(cè)試模塊預(yù)先配置了像來自 ?
@angular/platform-browser? 的 ?
BrowserModule?這樣的東西。
稍后你會(huì)用 ?
imports?、?
providers?和更多可聲明對(duì)象的參數(shù)來調(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?配置方法,無論是 ?
configureTestingModule()?、?
get()? 還是 ?
override...?方法都不行。如果你這樣做,?
TestBed?會(huì)拋出一個(gè)錯(cuò)誤。
?ComponentFixture ?是一個(gè)測(cè)試挽具,用于與所創(chuàng)建的組件及其對(duì)應(yīng)的元素進(jìn)行交互。
可以通過測(cè)試夾具(fixture)訪問組件實(shí)例,并用 Jasmine 的期望斷言來確認(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;
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 ?深入了解元素樹。
這是另一個(gè)調(diào)用 ?HTMLElement.querySelector? 來獲取段落元素并查找橫幅文本的測(cè)試:
it('should have with "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
const p = bannerElement.querySelector('p')!;
expect(p.textContent).toEqual('banner works!');
});
Angular 的測(cè)試夾具可以直接通過 ?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)境。你可以在非瀏覽器平臺(tái)上運(yùn)行這些測(cè)試,那些平臺(tái)上可能沒有 DOM,或者其模擬的 DOM 不支持完整的 ?HTMLElement ?API。
Angular 依靠 ?DebugElement ?抽象來在其支持的所有平臺(tái)上安全地工作。Angular 不會(huì)創(chuàng)建 HTML 元素樹,而會(huì)創(chuàng)建一個(gè) ?DebugElement ?樹來封裝運(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 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 ?樹中的一個(gè)節(jié)點(diǎn)與選擇條件匹配時(shí),該函數(shù)返回 ?true?。
你可以借助從庫(kù)中為運(yùn)行時(shí)平臺(tái)導(dǎo)入 ?By? 類來創(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 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)行過濾并且只測(cè)試瀏覽器原生元素的屬性時(shí),用 ?By.css? 方法可能會(huì)有點(diǎn)過度。
用 ?HTMLElement ?方法(比如 ?querySelector()? 或 ?querySelectorAll()?)進(jìn)行過濾通常更簡(jiǎn)單,更清晰。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流