在應用程式開發中,常會利用人工手動去測試系統的正確,不過當遇到較複雜的系統時,手動測試相對耗時且容易遺漏。除此之外,也可以撰寫單元測試 (Unit Testing) 程式並利用測試框架運行,來驗證系統的正確性;此通常針對的是方法或是類別,透過封裝隔離來縮小測試的範圍,以在持續開發與維護中確保程式品質。這一篇將了解在 Angular 應用程式如何針對元件進行單元測試 (Unit Test)。
Angular 官方建議使用 Jasmine 來撰寫測試程式,將測試程式撰寫在 *.spec.ts 檔案中,透過下列幾個方法來定義各種的測試案例。
describe('測試分組描述', () => {
beforeAll(() => { });
afterAll(() => { });
beforeEach(() => { });
it('測試案例描述', () => { });
afterEach(() => { });
});
每一個測試案例皆會寫在 it()
方法之中,且每一個案例皆不互相影響;當測試案例愈來愈多時,可以利用 describe()
方法來分組,此方法可以有一至多個 describe()
或 it()
方法。在執行 describe
下的所有測試案例之前,會觸發一次 beforeAll()
方法,此方法用於初始化所有案例所需的物件;當所有測試案例完成後,則會觸發一次 afterAll()
方法來注銷全域物件。而在每一個案例在執行的前後則分別會觸發 beforeEach()
與 afterEach()
方法。另外,可以在 describe()
或 it()
前加上 f,來只執行特定的測試案例;或是加上 x 來排除不執行該測試案例。
在執行測試時,Angular 會透過 TestBed 動態建立用來模擬 @NgModule
的測試模組,因此會在此針對所測試的元件對象匯入所需要的模組或元件。
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TaskComponent]
})
.compileComponents();
}));
大致了解 Jasmine 的方法後,首先在 task.component.spec.ts 中新增驗證主旨顯示的測試案例。
describe('TaskComponent', () => {
it('當指定主旨為 "頁面需要顯示待辦事項主旨", 頁面應顯示為 "頁面需要顯示待辦事項..."', () => {
});
});
在撰寫單元測試程式時會採用 3A 原則來加強測試程式的可讀性;此原則包含了:
describe('TaskComponent', () => {
it('當指定主旨為 "頁面需要顯示待辦事項主旨", 頁面應顯示為 "頁面需要顯示待辦事項..."', () => {
// arrange
const expected = '頁面需要顯示待辦事項...';
// act
// assert
});
});
另外,在對元件進行測試時,Angular 會利用 TestBed 建立元件的實體,並提供 ComponentFixture 物件來對元件與其 DOM 進行互動。因此此測試案例會針對 fixture
物件的元件實體變數 component
設定主旨屬性,並呼叫 detectChanges()
方法讓 TestBed 執行資料繫結。
describe('TaskComponent', () => {
it('當指定主旨為 "頁面需要顯示待辦事項主旨", 頁面應顯示為 "頁面需要顯示待辦事項..."', () => {
// arrange
const expected = '頁面需要顯示待辦事項...';
// act
component.subject = '頁面需要顯示待辦事項主旨';
fixture.detectChanges();
// assert
});
});
最後需要驗證頁面顯示的主旨是否符合預期,可以查詢 ComponentFixture 內的 DebugElement 樹來取得到 DebugElement 節點,進一步使用 nativeElement
來取得執行平台的原生物件,而指定的方式可以利用 css 選擇器或是直接指定元件類型。然後利用 Jasmine 提供的方法進行驗證後,就可以執行 ng test
來運行測試程式了。
describe('TaskComponent', () => {
it('當指定主旨為 "頁面需要顯示待辦事項主旨", 頁面應顯示為 "頁面需要顯示待辦事項..."', () => {
// arrange
const expected = '頁面需要顯示待辦事項...';
// act
component.subject = '頁面需要顯示待辦事項主旨';
fixture.detectChanges();
// assert
const debugElement = fixture.debugElement.query(By.css('div.content span'));
expect(debugElement.nativeElement.textContent).toContain(expected);
});
});
當元件有注入服務時,所撰寫單元測試一般不會直接使用實際的服務,而是建立一個 Spy 服務物件來模擬,如此才可以控制元件的輸入與輸出的狀態。不過,因為在 TaskList 元件中注入了 Router 服務元件,並且有使用到 Tsak 元件,所以在 task-list.component.spec.ts 中需要先匯入所需要的模組與元件。
describe('TaskListComponent', () => {
let component: TaskListComponent;
let fixture: ComponentFixture<TaskListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule, HttpClientModule, FormsModule],
declarations: [TaskListComponent, TaskComponent, TaskStateColorDirective],
}).compileComponents();
}));
});
接著,利用 Jasmine 提供的 createSpyObj 來建立一個待辦事項服務的 Spy 物件,且定義此服務元件 getData()
所回傳的值,並注入在 TestBed 內以取代實際的待辦事項服務。
describe('TaskListComponent', () => {
let component: TaskListComponent;
let fixture: ComponentFixture<TaskListComponent>;
let taskService: jasmine.SpyObj<TaskRemoteService>;
beforeEach(async(() => {
taskService = jasmine.createSpyObj(['getData']);
taskService.getData.and.returnValue(
of([
{
id: 1,
subject: '頁面需要顯示待辦事項主旨',
state: 0,
level: 'XS',
tags: ['FEATURE', 'ISSUE', 'enhancement', 'discussion'],
},
])
);
TestBed.configureTestingModule({
imports: [RouterTestingModule, HttpClientModule, FormsModule],
declarations: [TaskListComponent, TaskComponent, TaskStateColorDirective],
providers: [{ provide: TaskRemoteService, useValue: taskService }],
}).compileComponents();
}));
});
最後建立一個測試案例,直接利用 TaskComponent 來查詢頁面上所有 DebugElement 節點,並驗證其個數是否符合服務所設定的個數。
fit('應顯示一筆待辦事項', () => {
const debugElements = fixture.debugElement.queryAll(By.directive(TaskComponent));
expect(debugElements.length).toBe(1);
});
利用單元測試可以將測試案例記錄下,在持續的開發與維護中重覆的執行,減少人工測試的時間成本,更進一步的搭配 CI 來避免把有問題的程式更新至正式環境中。