[從 0 開始的 Angular 生活]No.45 在 Angular 內進行測試(五) - 帶有非同步服務的元件

前言

也有一種狀況是:依賴的服務元件資料的取得,是透過呼叫 API 等待伺服器吐資料的非同步行為,那麼這又該如何進行測試呢?

帶有非同步服務的元件

改寫上一個範例,當點選 welcomeComponent 元件內的登入按鈕時,會以 .subscribe() 的形式觸發 user 服務元件的 getData() 方法取得資料,最後顯示出歡迎提示,並且停用登入按鈕。

welcome.component 的 Template

1
2
3
<h3 class="welcome" *ngIf="data.isLoggedIn"><i>{{data.user}},{{data.message}}!</i></h3>
<h3 class="error" *ngIf="!data.isLoggedIn">未授權,請登入!</h3>
<button (click)="login()" [disabled]="data.isLoggedIn">登入</button>

welcome.component 的 class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component, OnInit } from '@angular/core';
import { UserService } from '../user.service';

@Component({
selector: 'app-welcome',
templateUrl: './welcome.component.html',
styleUrls: ['./welcome.component.scss']
})
export class WelcomeComponent implements OnInit {
data = {
isLoggedIn: false,
user: '',
message: ''
};
constructor(public userService: UserService) { }

ngOnInit() {
}
login() {
this.userService.getData().subscribe((result) => {
this.data = result;
});
}
}

user.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
providedIn: 'root'
})
export class UserService {
constructor() { }
getData() {
const data = {
isLoggedIn: false,
user: '',
message: ''
};
setTimeout(() => {
data.isLoggedIn = true;
data.user = 'Alvan';
data.message = '歡迎登入';
}, 2000);
return Observable.create(observer => observer.next(data));
}
}

如何測試帶有非同步服務的元件

這個範例測試的重點是:

  • 元件上的登入按鈕的 click 觸發事件是否有效
    • 意思是當透過點擊事件觸發元件內的 login() 時,方法真的有被呼叫
  • 元件的渲染是不是正常的
    • 意思是獲得資料後元件有正確的顯示

測試環境建置

於是我們可以像先前一樣,使用 jasmine.createSpyObj() 產生一個假的 getData() 方法,並且預先建立好一組假的資料 - data

welcome.component.spec

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

import { WelcomeComponent } from './welcome.component';
import { UserService } from '../user.service';

describe('WelcomeComponent', () => {
let component: WelcomeComponent;
let fixture: ComponentFixture<WelcomeComponent>;
let userService;
beforeEach(() => {
const spy = jasmine.createSpyObj('UserServiceSpy', ['getData']);
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
providers: [
{ provide: UserService, useValue: spy }
]
});
fixture = TestBed.createComponent(WelcomeComponent);
component = fixture.componentInstance;
userService = TestBed.get(UserService);
fixture.detectChanges();
el = fixture.nativeElement.querySelector('.welcome');
});

撰寫測試

前置作業準備完畢,開始撰寫測試吧。

第一個測試

  • 元件上的登入按鈕的 click 觸發事件是否有效
    • 意思是當透過點擊事件觸發元件內的 login() 時,方法真的有被呼叫
1
2
3
4
5
6
it('觸發 Click 事件後,是否有正常呼叫 getData() ', () => {
userService.getData.and.returnValue(new Observable());
// 模擬點擊
component.login();
expect(userService.getData).toHaveBeenCalled();
});

第二行的意思是,當假的 getData 方法被呼叫時必須回傳一個觀察者物件 (Observable) 。

因為第三行觸發元件內的 login 方法時

  • user 服務元件內的 getData() 被觸發了,並且使用 .subscribe() 方法訂閱
  • 而我們在測試時使用的 getData() 是假造的替身,所以必須回傳一個觀察者物件才可以使用 .subscribe() ,否則會出錯導致測試失敗

第二個測試

  • 測試元件的渲染是正常的
    • 意思是獲得資料後元件有正確的顯示
      • 像是當 isLoggedIntrue 時,登入按鈕為 disabled
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
it('未登入時元件的渲染', () => {
const el: HTMLElement = fixture.nativeElement.querySelector('.error');
expect(el.textContent).toContain('未授權,請登入!');
});
it('登入獲取資料後,元件的渲染', () => {
const data = {
isLoggedIn: true,
user: 'Alvan',
message: '歡迎登入'
};
component.data = data;
fixture.detectChanges();
const el: HTMLElement = fixture.nativeElement.querySelector('.welcome');
expect(el.textContent).toContain(data.user);
const btn: HTMLButtonElement = fixture.nativeElement.querySelector('button');
expect(btn.disabled).toBeTruthy();
});

而這部分測試的關鍵在於「順序」,像是:

  • component.data = data; 當我們把假資料重新賦值給元件內的 data
    • 必須呼叫 fixture.detectChanges(); 重新進行資料與元件間的繫結
      • 如此才可以使用 fixture.nativeElement.querySelector() 找到指定目標

fakeAsync() 進行非同步測試

fakeAsync()

在這個範例內,我並沒有實際的呼叫 API ,而是在 user 服務元件內透過 setTimeout() 模擬呼叫 API 時等待伺服器的時間。

而如果也想在測試過程中模擬這一段情境的話,可以使用 fakeAsync()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
it('非同步測試渲染情形', fakeAsync(() => {
let data = {
isLoggedIn: false,
user: '',
message: ''
};
const errEl: HTMLElement = fixture.nativeElement.querySelector('.error');
expect(errEl.textContent).toContain('未授權,請登入!');
setTimeout(() => {
data = {
isLoggedIn: true,
user: 'Alvan',
message: '歡迎登入'
};
component.data = data;
fixture.detectChanges();
}, 3000);
tick(3000);
const weEl: HTMLElement = fixture.nativeElement.querySelector('.welcome');
expect(weEl.textContent).toContain(data.user);
}));

這個測試使用了 setTimeout() 方法,令 data 物件內的屬性三秒後變更,並且重新賦值給元件內的 data 屬性,最後重新繫結。

而這個測試另一個關鍵是 tick()tick() 函式接受一個毫秒值作為參數(如果沒有提供則預設為 0)。

該參數表示虛擬時鐘要前進多少,也就是說:

  • setTimeout() 如果時間設置 3000 ,那麼 tick() 也要設置 3000

這樣才能正確取得資料。

tick() 時間參數設置小於 setTimeout() 的時間參數

至此完成了對元件的測試。

0%