希望是最好懂的 Angular 單元測試

Ray
35 min readAug 23, 2024

--

介紹

簡單介紹單元測試有什麼優點

  • 提早發現錯誤:以敏捷開發來說,程式很容易會頻繁變動,而要確保每次異動且其餘功能皆正常就可以加入單元測試(避免改A壞B情況)
  • 節省時間:通常每次開發完會需要測試當前功能是否正常,若都手動測試效率就不高,且可能會有情境缺漏沒測到
  • 程式說明:測試案例會讓看的人知道有什麼可以操作、步驟有哪些、預期的結果是什麼,這些也幫助開發跟測試更了解程式的用途

本文內容

  1. Angular 單元測試說明
  2. Jasmine 語法介紹
  3. 實際測試案例與執行
  4. 覆蓋率報告

Angular 單元測試說明

在Angular中常會使用到以下這行新增組件的指令

ng g c xxxComponent

這行指令是創建組件的指令,意思是在當前目錄下新增一個 xxxComponent 資料夾,其中預設會包含四個檔案,分別是:

xxxComponent.html
xxxComponent.scss
xxxComponent.ts
xxxComponent.spec.ts // 本文的主角是這個 .spec.ts。
// 補充:若新增組件的時候不想新增 .spec.ts,可以使用:
ng g c <componentName> --skip-tests

.spec.ts

用來編寫測試用例的文件。這些檔含包含了測試情境,描述希望測試的功能、預期的結果以及如何檢查這些結果,可以把它想程式一個檢查清單,當執行時會確認清單上的每一項功能是否正常執行。

舉例來說,現有一個登入頁面。我們可以在 .spec.ts 中確認以下幾項功能:帳號密碼輸入框是否可正常輸入、點擊登入按鈕後是否會檢查帳號密碼皆有輸入、登入成功或失敗後有沒有正常顯示對應的訊息。

既然我們知道 .spec.ts這個檔案是用來撰寫測試資料,那該如何撰寫?

Jasmine

.spec.ts 文件使用 TypeScript 編寫,並且基於 Jasmine 語法來撰寫測試用例。Jasmine 提供了一套通用的測試語法和功能,適用於 TypeScript 編寫的測試代碼。

以預設的.spec.ts來看,會有以下程式碼

import { ComponentFixture, TestBed } from '@angular/core/testing';

import { TestComponent } from './test.component';

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

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

fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

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

像是 describe就是 Jasmine 提供的函數,用來定義測試的套件;it 也是 Jasmine 提供的函數,用於定義測試案例; expect用於驗證結果是否符合預期(驗證的部分這邊簡單說明,後面第二部分語法介紹會再補充用法)

現在我們知道 .spec.ts 是基於 Jasmine 框架下撰寫測試案例,接下來如何需要測試運行起來?

Karma

Karma負責執行寫好的 .spec.ts測試案例,它會啟動瀏覽器並運行撰寫的測試案例,並告訴你測試是否通過。

Karma的設定可以在 angular.json中找到,預設設定如下:

  "test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}

builder:告訴 Angular 使用 Karma 工具來運行測試

options:配置測試時的其他設置(zone.js主要是負責一些異步操作。專案中可能很多操作都是異步的,例如呼叫API。zone.js確保 Angular 可以在操作完成後更新畫面)

  • tsConfig:指定提供測試用的設定檔,預設檔案是位於根目錄的tsconfig.spec.json
  • inlineStyleLanguage:指定內聯樣式的語言,預設與其他樣式文件一致
// 一般組件
import { Component } from '@angular/core';

@Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent {

}


// 內聯樣式
import { Component } from '@angular/core';

@Component({
selector: 'app-example',
template: `<div class="example">Hello World</div>`,
styles: [`
.example {
color: blue;
font-size: 20px;
}
`]
})
export class ExampleComponent {}
  • assets:指定靜態資源的路徑,沒設會導致測試運行時圖片沒有加載
  • styles:全局樣式,確保測試運行時的樣式與實際一致
  • scripts:可以在這邊載入第三方庫或自訂的 .js用於輔助測試

小結: spec.ts 文件是用來編寫測試的程式碼,這些測試根據 Jasmine 框架撰寫,Karma負責操作瀏覽器運行這些測試並驗證是否有如預期的執行。

Jasmine 語法介紹

介紹常見的一些 Jasmine 語法與範例情境,目錄如下:

  1. Jasmine 測試案例的集合 ( Test Suite )
  2. Jasmine 的 Hook
  3. Jasmine 的函數 (函數:可在任何地方被調用。不依賴於對象或類)
  4. Jasmine 的方法(方法:定義在對象或類中的函數。必須通過對象調用)

1. Jasmine 測試案例的集合 ( Test Suite )

describe

負責組織跟管理測試案例,可包含多個 Test Case。

(舉例來說:一個組件可能有多個功能,例如計算機可以分成加、減、乘、除四個功能,就可以各別建立一個 describe。)

describe('Calculator', () => {

describe('加', () => {
...
});

describe('減', () => {
...
});

describe('乘', () => {
...
});

describe('除', () => {
...
});

});

xdescribe

與 describe 是負責組織跟管理測試案例,但會在執行測試時暫時跳過(想保留這個 Test Suite 但這次執行測試時不想使用時可以使用)

// 不執行 MyComponent 這個 Test suite 所包含的所有測試函數
xdescribe('MyComponent', () => {
// 測試1
...

// 測試2
...
});

2. Jasmine 的 Hook

beforeAll

於測試開始前觸發,只觸發一次 (每個 describe 只能有一個)

describe('MyComponent', () => {
let apiResponse:any;
let connection:any;

// 範例情境1:像 API 不須在每次執行時呼叫,則可以寫在 beforeAll 中
beforeAll(async () => {
apiResponse = await fetchData();
});

});
describe('MyComponent', () => {
let apiResponse:any;
let connection:any;

// 範例情境2:連結 DB
beforeAll(() => {
connection = ...;
});

});

afterAll

於測試開始後觸發,只觸發一次 (每個 describe 只能有一個)

describe('MyComponent', () => {
let connection: any;

beforeAll(() => {
connection = ...;
});

// 範例情境:結束 DB 連結
afterAll(() => {
connection.connected = false;
});

});

beforeEach

於每個測試案例運行前觸發 (每個 describe 可以有多個,會按照順序執行)

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

// 範例情境1:初始化元件,讓每個測試案例執行前都是元件原始狀態
beforeEach(async () => {
// 指定要測試的元件,並將當前元件的 Template 在測試中編譯並正常渲染
await TestBed.configureTestingModule({
declarations: [MyComponent]
}).compileComponents();
// 創建元件實例
const fixture = TestBed.createComponent(MyComponent);
const component = fixture.componentInstance;
// 觸發變更檢測,確保Template與元件狀態都更新
fixture.detectChanges();
});


// 範例情境2:有些組件可能會依賴外部傳入的@Input(),可以在beforeEach中設定
beforeEach(() => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
component.inputProperty = 'test value'; // 設置 Input 屬性
fixture.detectChanges();
});


});

afterEach

於每個測試案例運行後觸發(每個 describe 可以有多個,會按照順序執行)

describe('MyComponent', () => {
let subscription: Subscription;

beforeEach(() => {
subscription = interval(1000).subscribe(() => {
...
});
});

// 範例情境:取消 beforeEach 的訂閱,避免 Memory leak
afterEach(() => {
subscription.unsubscribe();
});

});

3. Jasmine 的函數 (函數:可在任何地方被調用。不依賴於對象或類)

it

描述具體的測試場景,用於定義一個測試案例的函數

// 範例:測試該 Component 是否被建立  
it('should create', () => {
expect(component).toBeTruthy();
});

xit

僅是想在這次測試暫時跳過這個 Test Case,而不是刪掉時可以使用

// 不執行這個測試案例
xit('should create', () => {
expect(component).toBeTruthy();
});

fit

只要執行特定的測試時可以使用。所有其他的 it 測試用例會被跳過

describe('MyComponent', () => {

// 不會被執行
it('case1', () => {
...
});

// 不會被執行
it('case2', () => {
...
});

// 只會執行 fit 的這個測試案例
fit('case3', () => {
...
});

});

spyOn

監控物件上的方法呼叫。檢查方法是否被調用過、被調用的次數、傳遞的參數以及方法的返回值等


// calculator.ts
export class Calculator {
add(a: number, b: number): number {
return a + b;
}
}

// calculator.spec
import { Calculator } from './calculator';

describe('Calculator', () => {
let calculator: Calculator;

beforeEach(() => {
calculator = new Calculator();
});

it('spyOn測試', () => {

// 使用 spyOn 來監控 add 方法
spyOn(calculator, 'add');

// 調用 calculateSum 方法,這會間接調用 add 方法
calculator.calculateSum(1, 2);

// 範例測試1:驗證 add 方法是否被調用過
expect(calculator.add).toHaveBeenCalled();

// 範例測試2:檢查方法被調用的次數是否為 1 次
expect(calculator.add).toHaveBeenCalledTimes(1);

// 範例測試3:驗證 add 方法是否被調用時傳遞了正確的參數
expect(calculator.add).toHaveBeenCalledWith(1, 2);

});
});
// 範例測試4:設定 API 的回傳值

// spyOn 設定固定回傳值的情境舉例如以下:
// 當測試時不需要呼叫真正 API 拿資料時,可以透過 .returnValue 來設定固定返回值

// test.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
providedIn: 'root'
})
export class TestService {

private apiUrl = 'https://api.example.com/data';

constructor(private http: HttpClient) {}

getData(): Observable<any> {
return this.http.get(this.apiUrl);
}
}


// test.spec
describe('TestService', () => {
let service: TestService;
let http: jasmine.SpyObj<HttpClient>;

beforeEach(() => {
// 創建一個 HttpClient 的間諜對象
http = jasmine.createSpyObj('HttpClient', ['get']);

// 使用 TestBed 配置服務並注入間諜對象
TestBed.configureTestingModule({
providers: [
TestService,
{ provide: HttpClient, useValue: http }
]
});

service = TestBed.inject(TestService);
});

it('should return expected data from getData', () => {
const mockData = { id: 1, name: 'Test Data' };

// 設置 http.get 的返回值
http.get.and.returnValue(of(mockData));

// 調用 getData 方法
service.getData().subscribe(data => {
expect(data).toEqual(mockData);
// 這邊就可以用 mockData 繼續往下測
});

});
});

4. Jasmine 的方法(方法:定義在對象或類中的函數。必須通過對象調用)

expect

判斷傳入的參數是否符合條件,通常與其他比對規則方法一起用

it('should add two numbers correctly', () => {
const result = 2 + 3;
expect(result).toBe(5);
});

toBe

最常用的比較方法,但須要注意 toBe 使用的是嚴格比較(===),因此若類型不同或是比較物件或陣列時就需要注意

// 相當於 result === 5
it('should add two numbers correctly', () => {
const result = 2 + 3;
expect(result).toBe(5);
});

// 這個案例測試會失敗,因為類型不同
it('should add two numbers correctly', () => {
const result = 2 + 3;
expect(result).toBe("5");
});

toEqual

只會比較物件跟陣列的內容是否一致,跟 toBe 最大的差別在於 toEqual 不會看是否為同一個實例。

// fail
it('toBe_Test', () => {
expect({ a: 1 }).toBe({ a: 1 });
});

// success
it('toEqual_Test', () => {
expect({ a: 1 }).toEqual({ a: 1 });
});

若是比較基本類型的變數,不論是使用 toEqual 或 toBe 都會有相同結果,但如果是 object 或 array 則可能會有差異。toEqual 較著重於值是否相等,而 toBe 著重於判斷是否為同一個實例 (使用 toBe 的情境例如:確認有沒有重複建立實例,檢查資源是否有效利用,避免性能問題時可使用)。

not

用於反轉判斷的結果

// fail
it('test1', () => {
const result = 2 + 3;
expect(result).not.toBe(5);
})

// success
it('test2', () => {
const result = 2 + 3;
expect(result).not.toBe(6);
})

toContain

檢查一個集合(如陣列或字串)是否包含指定的元素或字串。

  // success
it('test1', () => {
expect("Ray").toContain("R");
});

// success
it('test2', () => {
expect([5, 6]).toContain(5);
});

// success
it('test3', () => {
expect([{ name: 'Ray' }, { name: 'Jack' }]).toContain({ name: 'Jack' });
});

toHaveBeenCalled

判斷要驗證的 Function 是否有被呼叫到

// 建立一個包含一個相加 function 的 Test.Service 

import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class TestService {

add(a: number, b: number): any {
return a + b;
}

}
// 針對這個 service 的 add 測試

import { TestService } from './test.service';

describe('TestService', () => {
// 初始化變數為目標類型
let service: TestService;

// 再每次測試前新建一個實例,確保不受其他測試影響
beforeEach(() => {
service = new TestService();
});

it('test', () => {

// 監控 TestService 的 add function
const addSpy = spyOn(service, 'add');

// 讓這個 service 執行一次
service.add(2, 5)

// 判斷 add() 是否執行過
expect(addSpy).toHaveBeenCalled();

});

});

toHaveBeenCalledTimes

判斷要驗證的 Function 是否被呼叫到 n 次

it('test', () => {

// 監控 TestService 的 add function
const addSpy = spyOn(service, 'add');

// 讓這個 service 執行一次
service.add(2, 5)

// 判斷 add() 是否執行過 "1" 次
expect(addSpy).toHaveBeenCalled(1);

});

toHaveBeenCalledWith

檢查 Function 是否用特定參數調用過

it('test', () => {

// 監控 TestService 的 add function
const addSpy = spyOn(service, 'add');

// 讓這個 service 執行一次
service.add(2, 5)

// 判斷 add() 是否在傳入 2,5 這組執行過一次
expect(addSpy).toHaveBeenCalledWith(2,5);

});

toThrow

判斷這個是否有執行到異常,可以用於傳入非預期參數時驗證報錯情境

it('test', () => {

function errorFunction() {
throw new Error('error message');
}

expect(errorFunction).toThrow();
});

toThrowError

判斷這個異常的錯誤訊息是否為預想

it('test', () => {

function errorFunction() {
throw new Error('error message');
}

expect(errorFunction).toThrowError('error message');
});

toMatch

檢查字串是否符合特定的正則表達式

it('test', () => {
expect('Angular').toMatch(/Angular/);
});

實際測試案例與執行

有什麼依據可以判斷一個組件需要寫哪些測試?

每個組件都是獨立的,所需要的測試也不同,但主要可從以下面向思考:

  • 組件功能(主要功能是什麼、會有什麼使用情境)
  • 例外狀況(空值或非合法值、超出預期的操作、錯誤處理是否正常)
  • 性能(API回覆過慢、資料過大、高負載、頻繁操作)
  • UI(畫面是否正常顯示、點擊按鈕是否觸發預期行為、i18n切換)

這邊我們建立一個登入畫面,接著對這個登入畫面寫測試案例並執行測試。

建立測試頁面

// login.html

<div class="login-container">
<h2>登入</h2>
<form (ngSubmit)="onSubmit(loginForm)" #loginForm="ngForm">
<div class="form-group">
<label for="username">帳號</label>
<input type="text" id="username" name="username" ngModel required #username="ngModel"
class="form-control" />
<div *ngIf="username.invalid && username.touched" class="error">
帳號必填
</div>
</div>
<div class="form-group">
<label for="password">密碼</label>
<input type="password" id="password" name="password" ngModel required #password="ngModel"
class="form-control" />
<div *ngIf="password.invalid && password.touched" class="error">
密碼必填
</div>
</div>
<button type="submit" [disabled]="loginForm.invalid" class="btn btn-primary">登入</button>
</form>
</div>
// login.component.ts

import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';

@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent {
constructor() { }

onSubmit(form: NgForm) {
if (form.valid) {
const { username, password } = form.value;
console.log('帳號:', username);
console.log('密碼:', password);
}
}
}

.spec 補充說明

接著是我們主要的 .spec 最一開始的樣子,加上了註解

// login.component.spec.ts

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';

// 描述 LoginComponent 的測試套件
describe('LoginComponent', () => {
// 定義 LoginComponent 實例和 ComponentFixture 的變數
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;

// 在每個測試之前執行的初始化代碼
beforeEach(async () => {
// 配置測試模塊
await TestBed.configureTestingModule({
// 聲明要測試的組件
declarations: [ LoginComponent ]
})
// 編譯組件的模板和樣式
.compileComponents();

// 創建 LoginComponent 的實例
fixture = TestBed.createComponent(LoginComponent);
// 獲取組件實例
component = fixture.componentInstance;
// 觸發變更檢測,更新視圖
fixture.detectChanges();
});

// 測試 LoginComponent 是否能夠正確創建
it('should create', () => {
// 驗證組件實例存在
expect(component).toBeTruthy();
});
});
  • fixture 類型的實例,封裝了組件及其模板的相關功能,雖然包含了組件的實例,但它主要是用來進行與模板相關的操作。(模板)
  • component 用於調用和測試組件的方法,驗證業務邏輯。(邏輯)

舉例來說,跟畫面上的 DOM 相關的操作就會使用 fixture

// 更新 DOM 以反映新輸入 
fixture.detectChanges();

要監控登入頁面的提交按鈕是否被執行,就會使用

// 監控 onSubmit 方法是否被調用
spyOn(component, 'onSubmit');

測試情境

預期對登入頁面做三個測試,分別為:

  1. 必填欄位(帳號、密碼)尚未填寫時,提交按鈕是否不可用
  2. 驗證表單狀態是否滿足(帳號、密碼皆有填寫),可以執行提交
  3. 提交之後,提交的 function 是否被執行

在寫測試之前,要先了解如何取得 DOM 元素,以及如何透過 Jasmine 替對應欄位輸入值,因此我們再稍微介紹一下 DOM 相關操作的語法。

// 用於查詢元素以及模板相關操作,通常與其他方法一起使用
fixture.debugElement

// 用於查找符合特定條件的元素 (ex.查找 id = username 的元素)
// 並取回對應的原生 DOM 元素
query(By.css('#username')).nativeElement


// 組合起來就可以獲得 id 為 username 的 DOM 元素
const usernameInput =
fixture.debugElement.query(By.css('#username')).nativeElement;


// 透過 .value 就可以更改輸入值,模擬用戶在該輸入框中輸入的數據
usernameInput.value = 'testuser';

// 模擬用戶輸入完數據之後,要讓 Angular 知道輸入框值已改變
// 讓 Angular 去觸發變更檢測機制,更新狀態跟模板
// 這樣可以觸發與用戶交互相關的事件處理程序,例如驗證、事件綁定等
usernameInput.dispatchEvent(new Event('input'));

接著回到測試案例

輸入帳號密碼前(圖左)、輸入帳號密碼後(圖右)
// 1.必填欄位(帳號、密碼)尚未填寫時,提交按鈕是否不可用
// 驗證帳號密碼皆有填時,則沒有錯誤提示(表單狀態正常)

describe('登入頁面', () => {

...

it('表單狀態驗證', () => {
// 取得表單的相關元素
const usernameInput = fixture.debugElement.query(By.css('#username')).nativeElement;
const passwordInput = fixture.debugElement.query(By.css('#password')).nativeElement;
const form = fixture.debugElement.query(By.css('form')).nativeElement;

fixture.detectChanges(); // 確保視圖更新
expect(form.checkValidity()).toBeFalse(); // 現在帳號密碼尚未輸入,狀態應為 False

// 輸入帳號、並手動觸發 Angular 檢測機制
usernameInput.value = 'testuser';
usernameInput.dispatchEvent(new Event('input'));

// 輸入密碼、並手動觸發 Angular 檢測機制
passwordInput.value = 'password123';
passwordInput.dispatchEvent(new Event('input'));

fixture.detectChanges(); // 確保視圖更新

expect(form.checkValidity()).toBeTrue(); // 帳號密碼皆已輸入,表單狀態應為 True
});

});
// 2.驗證表單狀態是否滿足(帳號、密碼皆有填寫),可以執行提交
// 驗證帳號密碼皆有填時, 登入按鈕是否可用

describe('登入頁面', () => {

...

it('登入按鈕驗證', () => {

// 取得表單的相關元素
const usernameInput = fixture.debugElement.query(By.css('#username')).nativeElement;
const passwordInput = fixture.debugElement.query(By.css('#password')).nativeElement;
const submitButton = fixture.debugElement.query(By.css('button')).nativeElement;

fixture.detectChanges();
expect(submitButton.disabled).toBeTrue(); // 此時欄位未填,驗證登入按鈕不可用

// 填入必填欄位資料
usernameInput.value = 'testuser';
usernameInput.dispatchEvent(new Event('input'));

passwordInput.value = 'password123';
passwordInput.dispatchEvent(new Event('input'));

fixture.detectChanges();

// 確認此時登入按鈕可填
expect(submitButton.disabled).toBeFalse();
});

});
// 3.提交之後,提交的 function 是否被執行
describe('登入頁面', () => {

...


it('登入按鈕點擊後是否作用', () => {
spyOn(component, 'onSubmit'); // 監控 onSubmit()

const usernameInput = fixture.debugElement.query(By.css('#username')).nativeElement;
const passwordInput = fixture.debugElement.query(By.css('#password')).nativeElement;
const formElement = fixture.debugElement.query(By.css('form')).nativeElement as HTMLFormElement;

// 輸入帳號密碼
usernameInput.value = 'testuser';
usernameInput.dispatchEvent(new Event('input'));
passwordInput.value = 'password123';
passwordInput.dispatchEvent(new Event('input'));

fixture.detectChanges();

// 執行提交
formElement.dispatchEvent(new Event('submit'));

// 確認監控的 onSubmit() 有被執行
expect(component.onSubmit).toHaveBeenCalled();
});

});

測試案例寫完,接著在 Terminal 執行 ng test後 Karma 會啟動,查看測試結果。

覆蓋率報告

測試覆蓋率報告建立

Code coverage report

透過於 Terminal 輸入 ng test --no-watch --code-coverage 生成,預設於根目錄產生 coverage 資料夾,其中 index.html 為主報告文件。

  • -no-watch : 執行完一次後就退出
  • -code-coverage : 產生覆蓋率報告

如果覺得每次都要輸入 ng test --no-watch --code-coverage 太麻煩,可以在 angular.json 中 test 底下新增 codeCoveragecodeCoverageExclude,這樣會讓每次執行 ng test時自動產生報告。


...

"architect": {
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
...
"codeCoverage": true,
"codeCoverageExclude": [
"**/test-setup.ts",
"**/environments/**"
]
},
...
}
}

...
  • codeCoverage: 設置為 true 表示每次執行 ng test時自動產生報告
  • codeCoverageExclude: 指定不計入覆蓋率的文件或目錄 ( 例如:測試設置文件和環境文件 )

測試覆蓋率報告說明

報告內容主要會顯示

  • 語句覆蓋率(Statement):測試中執行的語句比例 ( Component 有多少獨立語句的比例被執行 )
  • 分支覆蓋率(Branch):測試中涵蓋的條件分支比例 ( 一個 If 與 else,若只有測到其中一個,則分支覆蓋率就是 50%)
  • 函數覆蓋率(Function):測試中執行的函數比例 ( Component 中有 10個 Function , 測試時執行過 6 個,那麼函數覆蓋率就是 60%)
  • 行覆蓋率(Line):測試中執行的程式碼行比例。( Component 有 10行,測試時執行過其中 6 行,那麼語句覆蓋率就是 60% )

語句覆蓋率跟行覆蓋率比較容易搞混,看舉例比較好懂,範例一如下:

// test.component.ts

import { Component } from '@angular/core';

@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent {
constructor() { }

test(a: boolean): boolean {
if (a === true) { return a } else { return a; }
}

}
// test.component.spec.ts
describe('測試', () => {
...

it('test', () => {
component.test(false)
});

});
範例1 測試報告

若是 test.component.spec 不異動,但將 test.component 改成如下:

import { Component } from '@angular/core';

@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent {
constructor() { }

test(x: boolean): string {
if (x) {
return 'is true'
}
else {
return 'is false';
}
}

}
範例2 測試報告

發現差異了嗎?主要就在 Statements 不會受縮排影響,會以語句來判斷,而 Lines 只要透過縮排就會影響報告結果,即便邏輯是一樣的。

另外既然都舉範例程式碼了,可以一併看一下 Branches 與 Functions 的數據是為何?

Functions 為 100% 是因為 TestComponent 僅有一個 Function 為 test(),而測試中有寫到 component.test(false) 因此這個 Function 有執行到,所以為 100%。

Branches是 test() 這個 Function 有兩條線,分別是 根據傳入為 true 或 false 決定走,但是測試只有測到 false 的這條,所以為 50%,若要提升為 100% 只需要讓兩條線都有走到即可

  it('test', () => {
component.test(false)
});

it('test', () => {
component.test(true)
});
針對 Branches 調整後測試結果

--

--

No responses yet