[從 0 開始的 Angular 生活]No.42 在 Angular 內進行測試(二) - 元件

前言

介紹完服務元件的測試後,接著要學習如何測試一個單純的元件。

元件測試

在 Angular 中,普通的元件包含了 Template 與 class ,因此想對元件進行充分的測試,勢必得對這兩個部分都進行測試才行。

這些測試需要在瀏覽器的 DOM 中建立元件的宿主元素(就像 Angular 所做的那樣),然後檢查元件的 class 和 DOM 的互動是否如同 Template 中所描述的那樣。

Angular 的 TestBed 為所有這些型別的測試提供了基礎設施。

但是很多情況下,可以單獨測試元件類本身而不必涉及 DOM ,就已經可以用一種更加簡單、清晰的方式來驗證該元件的大多數行為了。

建立一個最單純的元件

測試之前我們要先準備環境,因此建立一個元件 - lightSwitch

  • 當用戶點選按鈕時,它會切換燈的開關狀態 (畫面上的文字狀態)

輸入 ng g c lightSwitch 建立元件。

Template

1
2
<button (click)="clicked()">Click me!</button>
<span>{{getMessage()}}</span>

class

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

@Component({
selector: 'app-light-swich',
templateUrl: './light-swich.component.html',
styleUrls: ['./light-swich.component.scss']
})
export class LightSwichComponent implements OnInit {
isOn = false;
constructor() { }
ngOnInit() {}
clicked() {
this.isOn = !this.isOn;
}
getMessage() {
return `燈現在是 ${this.isOn ? '開' : '關'} 的!`;
}
}

app.component Template

1
<app-light-swich></app-light-swich>

成功運行

測試元件的 class

可以像先前測試服務元件般,單獨測試元件中的 class 。

  • 這個範例中要測試 clicked() 方法能否正確切換燈的開關狀態
  • getMessage() 有無回傳合適的訊息

而這個元件的 class 並沒有依賴任何的服務元件,是非常單純的。

這種情況下可以直接 new 出物件實體,進行 isOn 屬性的狀態測試。

light-swich.component.spec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { LightSwichComponent } from './light-swich.component';

describe('LightSwichComponent', () => {
it('點擊 clicked() 後的狀態測試', () => {
const comp = new LightSwichComponent();
expect(comp.isOn).toBe(false, '一開始的狀態是 false');
comp.clicked();
expect(comp.isOn).toBe(true, '按下後的狀態是 true');
comp.clicked();
expect(comp.isOn).toBe(false, '再按一次回到 false');
});

it('點擊 clicked() 後 Message 會顯示 "開"', () => {
const comp = new LightSwichComponent();
expect(comp.getMessage()).toMatch(/關/i, '一開始的狀態是關的');
comp.clicked();
expect(comp.getMessage()).toMatch(/開/i, '按下後為開');
});
});

測試成功

故意出錯的情況

測試元件的 DOM

完整的元件不只有 class ,元件還要和 DOM 以及其它元件進行互動。

只涉及 class 的測試可以得知元件 class 的行為是否正常,但不能得知元件是否能正常渲染出來、響應使用者的輸入和查詢或與它的父元件和子元件相整合。

要進行完整的測試,我們不得不建立那些與元件相關的 DOM 元素了,必須檢查 DOM 來確認元件的狀態能在恰當的時機正常顯示出來,並且必須透過螢幕來模擬使用者的互動,以判斷這些互動是否如我們預期。

而這部分就需要用到 TestBed 了。

當我們建立元件時, Angular CLI 會幫我們寫好預設的測試

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
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { LightSwichComponent } from './light-swich.component';

describe('LightSwichComponent', () => {
let component: LightSwichComponent;
let fixture: ComponentFixture<LightSwichComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LightSwichComponent ]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(LightSwichComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeDefined();
});
});

而有些時候並不需要這些 Angular CLI 幫我們寫好的 Code 。
Angular CLI 是預設我們可能會用到這些東西,而目前我們可以再精簡一些。

1
2
3
4
5
6
7
8
9
10
describe('LightSwichComponent (minimal)', () => {
it('should create', () => {
TestBed.configureTestingModule({
declarations: [ LightSwichComponent ]
});
const fixture = TestBed.createComponent(LightSwichComponent);
const component = fixture.componentInstance;
expect(component).toBeDefined();
});
});

當元件逐漸演變成更加實質性的東西時,才會用到那些 Angular CLI 幫我們預設產生的測試。

在這個例子中,傳給 TestBed.configureTestingModule 的元資料物件中只宣告了 LightSwichComponent - 也就是待測試的元件。

不用宣告或匯入任何其它的東西,預設的測試模組中已經預先配置好了,
比如來自 @angular/platform-browser 的 BrowserModule。

createComponent()

在配置好 TestBed.configureTestingModule() 之後,可以呼叫它的 createComponent() 方法。

TestBed.createComponent() 會建立一個 LightSwichComponent 的元件,把相應的元素新增到 test-runner 的 DOM 中,然後返回一個 ComponentFixture 物件

特別要注意的是,在呼叫了 createComponent 之後就不能再重新配置 TestBed 了。 createComponent 方法凍結了當前的 TestBed 定義,關閉它才能再進行後續配置。

也就是說不能:

  • 呼叫任何 TestBed 的後續配置方法
  • 不能調 configureTestingModule()
  • 不能調 get()
  • 能呼叫任何 override … 方法

如果試圖這麼做,TestBed 就會丟擲錯誤。

ComponentFixture

ComponentFixture 是用來與所建立的元件及其 DOM 元素進行互動。

從剛才的範例程式碼來看,我們可以透過 fixture 來訪問該元件的 instance ,並用 Jasmine 的 expect 語句來確保其存在。

  • .toBeDefined()
    • 使用 .toBeDefined() 檢查一個變數不是 undefined 。

beforeEach()

隨著元件的成長,可能會有很多組測試。

這時除了一直複製之外,還有個比較好的做法:

  • 把重複會用到的程式碼搬到 beforeEach() 內。

因此可以重新調整剛剛那段程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
describe('LightSwichComponent (minimal)', () => {
let component: LightSwichComponent;
let fixture: ComponentFixture<LightSwichComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ LightSwichComponent ]
});
fixture = TestBed.createComponent(LightSwichComponent);
component = fixture.componentInstance;
});

it('should create', () => {
expect(component).toBeDefined();
});
});

nativeElement

可以使用 nativeElement 中獲取元件內的元素,像是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
describe('LightSwichComponent (minimal)', () => {
let component: LightSwichComponent;
let fixture: ComponentFixture<LightSwichComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ LightSwichComponent ]
});
fixture = TestBed.createComponent(LightSwichComponent);
component = fixture.componentInstance;
});

it('should create', () => {
expect(component).toBeDefined();
});

it('檢查元件的元素內有沒有包含 按鈕(button)', () => {
const el: HTMLElement = fixture.nativeElement;
console.log(el);
console.log(el.getElementsByTagName('button').length);
expect(el.getElementsByTagName('button').length).toBeGreaterThan(0);
});
});

console.log(el); 印出來的內容是:

於是可以藉由 .getElementsByTagName() 找到 button 標籤,最後再判斷長度,即可測試這個元件內有沒有按鈕了。

DebugElement

而除了使用 nativeElement 取得元件內的元素之外,也可以透過 DebugElement 取得元件內的元素。

至於為什麼要使用 DebugElement 呢?

以下是官方文件給出的解釋:

nativeElement 的屬性取決於執行環境。 你可以在沒有 DOM,或者其 DOM 模擬器無法支援全部 HTMLElement API 的平臺上執行這些測試。Angular 依賴於 DebugElement 這個抽象層,就可以安全的橫跨其支援的所有平臺。 Angular 不再建立 HTML 元素樹,而是建立 DebugElement 樹,其中包裹著相應執行平臺上的原生元素。 nativeElement 屬性會解開 DebugElement,並返回平臺相關的元素物件。

所以上面那個使用 nativeElement 的測試可以改寫成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
describe('LightSwichComponent (minimal)', () => {
let component: LightSwichComponent;
let fixture: ComponentFixture<LightSwichComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ LightSwichComponent ]
});
fixture = TestBed.createComponent(LightSwichComponent);
component = fixture.componentInstance;
});

it('should create', () => {
expect(component).toBeDefined();
});

it('檢查元件的元素內有沒有包含按鈕 (button)', () => {
const elDebug: DebugElement = fixture.debugElement;
const el: HTMLElement = elDebug.nativeElement;
console.log(el);
console.log(el.getElementsByTagName('button').length);
expect(el.getElementsByTagName('button').length).toBeGreaterThan(0);
});
});

改寫後的結果會與改寫前完全一樣。

小結

實務上的元件可能不會像範例上一樣這麼單純,可能會依賴某個服務元件之類的…情況,因為再寫下去可能篇幅會過長,所以決定先在這邊設個中斷點。

0%