[從 0 開始的 Angular 生活]No.44 在 Angular 內進行測試(四) - 有依賴關係的元件

前言

前一篇大致理解了如何測試一個單純的元件或是帶有繫結的元件。但在實務的應用上,元件經常依賴著其他服務元件,因此這個例子要實作的範例是 - 如何測試一個帶有依賴的元件?

  • 在這個範例中,一個帶有依賴的元件可以拆成
    • 製作一個 welcome 元件 + 一個負責提供資料的 user 服務元件

環境建立

輸入 ng g c welcome -m app 在產生 welcome 元件後將其註冊到 app.module 內。

輸入 ng g s user 產生 user 服務元件

app.module 註冊 user 服務元件

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
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { DemoService } from './demo.service';
import { Demo2Service } from './demo2.service';
import { LightSwichComponent } from './light-swich/light-swich.component';
import { BannerComponent } from './banner/banner.component';
import { WelcomeComponent } from './welcome/welcome.component';
import { UserService } from './user.service';

@NgModule({
declarations: [
AppComponent,
LightSwichComponent,
BannerComponent,
WelcomeComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [DemoService, Demo2Service, UserService],
bootstrap: [AppComponent]
})
export class AppModule { }

基本的檔案建立完成後,接著就是撰寫程式碼了。

user.service

1
2
3
4
5
6
7
8
9
10
11
12
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class UserService {
isLoggedIn = true;
user = {
name: 'Alvan'
};
constructor() { }
}

在服務元件內建立 isLoggedIn 屬性以及 user 物件,這是待會要提供給 welcomeComponent 的內容。

welcome Template

1
<h3 class="welcome"><i>{{welcome}}</i></h3>

welcome 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';
import { UserService } from '../user.service';

@Component({
selector: 'app-welcome',
templateUrl: './welcome.component.html',
styleUrls: ['./welcome.component.scss']
})
export class WelcomeComponent implements OnInit {
welcome: string;
constructor(private userService: UserService) { }

ngOnInit() {
this.welcome = this.userService.isLoggedIn ?
`歡迎, ${this.userService.user.name}` : `未授權, 請登入!`;
}

}

最後把 app-welcome 標籤加到 app.component.html 。

1
<app-welcome></app-welcome>

運行開發伺服器,看看執行結果。

isLoggedIn 為 true 的情況

isLoggedIn 為 false 的情況

測試帶有依賴的元件

在這個範例裡 WelcomeComponent 依賴著 user 服務元件的資料,接收到資料後根據 isLoggedIn 的狀態決定顯示的語句。

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
24
25
26
27
28
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>;
const userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
providers: [
{ provide: UserService, useValue: userServiceStub }
]
});
fixture = TestBed.createComponent(WelcomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

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

這裡跟之前練習有依賴關係的服務元件的測試時有點像,我們在 TestBed.configureTestingModule 內宣告了:

  • 待測試的元件 - WelcomeComponent
  • 在 providers 陣列中添加了 UserService 服務元件
    • 並且透過 useValue 將 UserService 的資料換成測試用的資料

被測試的元件不一定要注入真正的服務,可以是個模擬或偽造出來的資料。

這裡的主要目的是測試元件,而不是服務。

有些時候,服務元件可能連自身都有問題,不應該讓它干擾對元件的測試。

注入真實的 UserService 有可能很麻煩,像是:

  • 真實的服務可能詢問使用者登入憑據
  • 也可能試圖連線認證伺服器

這樣會很難處理這些行為,所以建立和註冊 UserService 替身 (userServiceStub) ,會讓測試更加容易。

獲得注入的服務

我們製作了一個 UserService 的替身 - userServiceStub ,那麼該如何取用它呢?

Angular 的注入系統是層次化的。

可以有很多層注入器,從根 TestBed 建立的注入器下來貫穿整個元件樹。

因此這邊有兩種做法:

第一種做法 - 最安全並有效的獲取注入服務的方法:

  • 從被測元件的注入器獲取
  • 元件注入器是 fixture 的 DebugElement 的屬性之一。
1
2
// UserService actually injected into the component
userService = fixture.debugElement.injector.get(UserService);

第二種做法 - 透過 TestBed.get() 來使用根注入器獲取該服務:

1
2
// UserService from the root injector
userService = TestBed.get(UserService);

不過這只有當 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
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 userServiceStub;
beforeEach(() => {
// 模擬物件
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
providers: [
{ provide: UserService, useValue: userServiceStub }
]
});
fixture = TestBed.createComponent(WelcomeComponent);
component = fixture.componentInstance;

// 獲取注入的服務
const userService = fixture.debugElement.injector.get(UserService);
const el = fixture.nativeElement.querySelector('.welcome');
});

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

加入一些測試例

接著補上一些測試例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
it('should welcome the user', () => {
fixture.detectChanges();
const content = el.textContent;
expect(content).toContain('歡迎');
expect(content).toContain('Test User');
});

it('should welcome "Bubba"', () => {
// 變更姓名
userService.user.name = 'Bubba';
fixture.detectChanges();
expect(el.textContent).toContain('Bubba');
});
it('should request login if not logged in', () => {
userService.isLoggedIn = false;
fixture.detectChanges();
const content = el.textContent;
// 檢查字串是不是不包含 "歡迎"
expect(content).not.toContain('歡迎');
expect(content).toBe('未授權, 請登入!');
});
  • 第一個測試檢查 el.textContent 有沒有包含「歡迎」、「Test User」
  • 第二個測試檢查當變更姓名時,顯示是否仍包含「Bubba」
  • 第三個測試檢查當未授權登入時,顯示是否不包含「歡迎」
    • 且顯示文字「未授權, 請登入!」

測試通過

刻意失敗

0%