介紹
簡單介紹單元測試有什麼優點
- 提早發現錯誤:以敏捷開發來說,程式很容易會頻繁變動,而要確保每次異動且其餘功能皆正常就可以加入單元測試(避免改A壞B情況)
- 節省時間:通常每次開發完會需要測試當前功能是否正常,若都手動測試效率就不高,且可能會有情境缺漏沒測到
- 程式說明:測試案例會讓看的人知道有什麼可以操作、步驟有哪些、預期的結果是什麼,這些也幫助開發跟測試更了解程式的用途
本文內容
- Angular 單元測試說明
- Jasmine 語法介紹
- 實際測試案例與執行
- 覆蓋率報告
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 語法與範例情境,目錄如下:
- Jasmine 測試案例的集合 ( Test Suite )
- Jasmine 的 Hook
- Jasmine 的函數 (函數:可在任何地方被調用。不依賴於對象或類)
- 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');
測試情境
預期對登入頁面做三個測試,分別為:
- 必填欄位(帳號、密碼)尚未填寫時,提交按鈕是否不可用
- 驗證表單狀態是否滿足(帳號、密碼皆有填寫),可以執行提交
- 提交之後,提交的 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 會啟動,查看測試結果。
覆蓋率報告
測試覆蓋率報告建立
透過於 Terminal 輸入 ng test --no-watch --code-coverage
生成,預設於根目錄產生 coverage 資料夾,其中 index.html 為主報告文件。
- -no-watch : 執行完一次後就退出
- -code-coverage : 產生覆蓋率報告
如果覺得每次都要輸入 ng test --no-watch --code-coverage
太麻煩,可以在 angular.json 中 test
底下新增 codeCoverage
與 codeCoverageExclude
,這樣會讓每次執行 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)
});
});
若是 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';
}
}
}
發現差異了嗎?主要就在 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)
});