[從 0 開始的 Angular 生活]No.43 在 Angular 內進行測試(三) - 繫結元件

前言

同樣是元件的測試,這次試著假設一些不同的狀況,練習如何對這些元件進行測試。

元件的繫結 (一) - 內嵌繫結

建立一個 BannerComponent 並且透過繫結到元件的 title 屬性來展示動態標題。

輸入 ng g c banner -m app 產生 BannerComponent 並且於 app.module 內註冊。

class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-banner',
templateUrl: './banner.component.html',
styleUrls: ['./banner.component.scss']
})
export class BannerComponent implements OnInit {
title = 'This is Title';
constructor() { }

ngOnInit() {
}

}

Template

1
<h1>{{title}}</h1>

app.component

1
<app-banner></app-banner>

將會寫一系列測試來探查 h1 標籤的值。

首先在 beforeEach() 中使用標準的 HTML querySelector 來找到該元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
describe('BannerComponent', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let h1: HTMLElement;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
});
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
h1 = fixture.nativeElement.querySelector('h1');
});
it('最終呈現在網頁上的標題文字是否與元件內 title 屬性的值相同', () => {
expect(h1.textContent).toContain(component.title);
});
});

測試看看!

createComponent() 函式不會繫結資料,因為繫結是在 Angular 執行變更檢測時才發生的。

解法 - 補上 detectChanges()

TestBed.createComponent 不能觸發變更檢測,所以要補上 detectChanges() 。

透過呼叫 fixture.detectChanges() 來要求 TestBed 執行資料繫結。

1
2
3
4
it('最終呈現在網頁上的標題文字是否與元件內 title 屬性的值相同', () => {
fixture.detectChanges();
expect(h1.textContent).toContain(component.title);
});

這種變更檢測是故意設計的,它給了測試者一個機會:

  • 在 Angular 初始化資料繫結以及呼叫生命週期的鉤子之前探查並改變元件的狀態

像是我們可以這麼做:

1
2
3
4
5
it('最終呈現在網頁上的標題文字是否與元件內 title 屬性的值相同', () => {
component.title = 'Test Title';
fixture.detectChanges();
expect(h1.textContent).toContain(component.title);
});

在呼叫 fixture.detectChanges() 之前修改元件的 title 屬性。

自動變更檢測

BannerComponent 的這些測試需要頻繁呼叫 detectChanges() ,而 Angular 測試環境可以做到自動執行變更檢測,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
describe('BannerComponent', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let h1: HTMLElement;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
providers: [
{provide: ComponentFixtureAutoDetect, useValue: true}
]
});
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
h1 = fixture.nativeElement.querySelector('h1');
});
it('should display original title', () => {
// Hooray! No `fixture.detectChanges()` needed
expect(h1.textContent).toContain(component.title);
});
it('should still see original title after comp.title change', () => {
const oldTitle = component.title;
component.title = 'Test Title';
console.log('第二個測試例', h1.textContent, oldTitle);
// Displayed title is old because Angular didn't hear the change :(
expect(h1.textContent).toContain(oldTitle);
});
it('should still see original title after comp.title change', () => {
const oldTitle = component.title;
component.title = 'Test Title';
fixture.detectChanges();
console.log('第三個測試例', h1.textContent, oldTitle);
expect(h1.textContent).toContain(oldTitle);
});
});

第一個測試的例子展示了自動 detectChanges() 的好處。

第二、三個例子要說明的是,Angular 測試環境不會知道測試程式改變了元件的 title 屬性。

自動檢測只對非同步行為比如承諾的解析、計時器和 DOM 事件作出反應,直接修改元件屬性值是不會觸發自動檢測的。

測試程式必須手動呼叫 detectChange(),來觸發新一輪的變更檢測週期。

元件的繫結 (二) - 模擬使用者輸入

修改剛剛的範例,替這個元件增加一個 Input 輸入,根據輸入來變化標題。

banner Template

1
2
<h1>{{title}}</h1>
<input type="text" [(ngModel)]="title">

接著需要到 app.module 內 import FormsModule 才可以使用雙向繫結

使用 dispatchEvent() 修改輸入值

如果想在測試時模擬使用者輸入,你就要找到 input 元素並設定它的 value 屬性。

而 Angular 不知道我們設定了 input 元素的 value 屬性,所以需要先呼叫:

  • dispatchEvent()
  • 再呼叫 detectChanges()

最後別忘了同樣也必須在 TestBed.configureTestingModule 內 import FormsModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
describe('BannerComponent', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
imports: [
FormsModule
],
providers: [
{provide: ComponentFixtureAutoDetect, useValue: true}
]
});
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
});

it('比較輸入與標題是否一致', () => {
const hostElement = fixture.nativeElement;
const titleInput: HTMLInputElement = hostElement.querySelector('input');
const titleDisplay: HTMLElement = hostElement.querySelector('h1');

// 模擬使用者輸入的值
titleInput.value = 'DCFGBHNJK';
titleInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(titleDisplay.textContent).toBe('DCFGBHNJK');
});
});

測試通過

0%