今天我們要來為我們用 Template Driven Forms 所撰寫的被保人表單寫單元測試,如果還沒有相關程式碼的朋友,趕快前往閱讀第十天的文章: Template Driven Forms 實作 - 動態表單初體驗。
此外,由於許多同樣的事情已在第六天的文章:單元測試實作 - 登入系統 by Template Driven Forms 講過了,例如前置作業的部份,我就不再重複贅述囉!
個人習慣要撰寫測試時的第一件事情,就是先把目標類別的依賴都先準備好,例如我們的被保人表單至少會需要 FormsModule
。
import { TestBed } from '@angular/core/testing';
import { TemplateDrivenFormsAsyncInsuredComponent } from './template-driven-forms-async-insured.component';
describe('TemplateDrivenFormsAsyncInsuredComponent', () => {
let component: TemplateDrivenFormsAsyncInsuredComponent;
let fixture: ComponentFixture<TemplateDrivenFormsAsyncInsuredComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TemplateDrivenFormsAsyncInsuredComponent],
imports: [FormsModule]
}).compileComponents();
fixture = TestBed.createComponent(TemplateDrivenFormsAsyncInsuredComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
加完之後使用 ng test
的指令將測試程式啟動起來,應該要能通過我們的第一個測試案例 should create
。
雖然有些人可能會發現,當前這個階段如果不加不會報錯,其實這是因為我們的程式一開始沒有任何表單(空陣列),一旦後續測試時加了之後一定會報錯噢!
再次幫大家複習:單元測試主要是要用來驗證單個類別的函式其實際執行結果是否符合我們預期的執行結果。
開始前先打開 .ts
來看一下要寫哪些案例:
export class TemplateDrivenFormsAsyncInsuredComponent {
// 被保險人清單
insuredList: Insured[] = [];
/**
* 根據索引來重新渲染有更改的節點
* 詳情請參考官方文件:https://angular.tw/api/common/NgForOf
*
* @param {string} index
* @return {*} {number}
* @memberof AppComponent
*/
trackByIndex(index: number): number {
return index;
}
/**
* 綁定在姓名欄位上,當使用者改變被保險人的姓名時,會觸發此函式,並取得對應的錯誤訊息
*
* @param {string} name
* @param {ValidationErrors | null} errors
* @param {Insured} insured
* @memberof TemplateDrivenFormsAsyncInsuredComponent
*/
insuredNameChange(name: string, errors: ValidationErrors | null, insured: Insured): void {
insured.name = name;
insured.nameErrorMessage = this.getErrorMessage(errors);
}
/**
* 綁定在年齡欄位上,當使用者改變被保險人的年齡時,會觸發此函式,並取得對應的錯誤訊息
*
* @param {string} age
* @param {ValidationErrors | null} errors
* @param {Insured} insured
* @memberof TemplateDrivenFormsAsyncInsuredComponent
*/
insuredAgeChange(age: string, errors: ValidationErrors | null, insured: Insured): void {
insured.age = age;
insured.ageErrorMessage = this.getErrorMessage(errors);
}
/**
* 新增被保險人
*
* @memberof TemplateDrivenFormsAsyncInsuredComponent
*/
addInsured(): void {
const insured: Insured = {
name: '',
gender: '',
age: '',
nameErrorMessage: '',
ageErrorMessage: ''
};
this.insuredList.push(insured);
}
/**
* 刪除被保險人
*
* @param {number} index
* @memberof TemplateDrivenFormsAsyncInsuredComponent
*/
deleteInsured(index: number): void {
this.insuredList.splice(index, 1);
}
/**
* 根據 FormControl 的 errors 屬性取得相應的錯誤訊息
*
* @private
* @param {ValidationErrors | null} errors - FormControl 的 errors
* @return {*} {string}
* @memberof TemplateDrivenFormsAsyncInsuredComponent
*/
private getErrorMessage(errors: ValidationErrors | null): string {
let errorMessage = '';
if (errors?.required) {
errorMessage = '此欄位必填';
} else if (errors?.minlength) {
errorMessage = '姓名至少需兩個字以上';
}
return errorMessage;
}
}
以目前的程式碼來看,我們要測的單元有 trackByIndex
、 insuredNameChange
、 insuredAgeChange
、 addInsured
與 deleteInsured
這五個,接下來我們照順序先從 trackByIndex
來寫好了。
這個測試單元非常簡單,不多說直接看程式碼:
describe('trackByIndex', () => {
it('should just return the index', () => {
// Arrange
const index = 0;
// Assert
expect(component.trackByIndex(index)).toBe(index);
})
});
測試結果:
接下來要測的單元是 insuredNameChange
,要測的案例有:
name
的值賦值給傳入的 insured
裡的 name
。errors
有 required
欄位,則會將錯誤訊息 此欄位必填
賦值給傳入的 insured
裡的 nameErrorMessage
。errors
有 minlength
欄位,則會將錯誤訊息 姓名至少需兩個字以上
賦值傳入的 insured
裡的 nameErrorMessage
。程式碼如下:
describe('insuredNameChange', () => {
let insured: Insured;
beforeEach(() => {
insured = {
name: '',
gender: '',
age: '',
nameErrorMessage: '',
ageErrorMessage: ''
};
});
it('should assign the value of the formControl to property "name" of the insured', () => {
// Arrange
const value = 'Leo';
const errors = null;
// Act
component.insuredNameChange(value, errors, insured);
// Assert
expect(insured.name).toBe(value);
});
it('should assign error message "此欄位必填" to property "nameErrorMessage" of the insured when the value of the formControl is empty string', () => {
// Arrange
const value = '';
const errors = { required: true };
const errorMessage = '此欄位必填';
// Act
component.insuredNameChange(value, errors, insured);
// Assert
expect(insured.nameErrorMessage).toBe(errorMessage);
});
it('should assign error message "姓名至少需兩個字以上" to property "nameErrorMessage" of the insured when the value\;s length of the formControl less than 2', () => {
// Arrange
const value = 'L';
const errors = {
minlength: {
actualLength: 1,
requiredLength: 2
}
};
const errorMessage = '姓名至少需兩個字以上';
// Act
component.insuredNameChange(value, errors, insured);
// Assert
expect(insured.nameErrorMessage).toBe(errorMessage);
});
});
這邊的程式碼大家應該都還算熟悉,比較特別需要提醒的是,記得要把初始化這件事寫在 beforeEach
裡,讓每個測試案例在執行之前都能拿到重新初始化過後的值,避免與其他的測試案例共用同個資料或物件。
測試結果:
下個要測的單元是 insuredAgeChange
,基本上跟 insuredNameChange
相似度高達 87% ,要測試的案例有:
age
的值賦值給傳入的 insured
裡的 name
。errors
有 required
欄位,則會將錯誤訊息 此欄位必填
賦值給傳入的 insured
裡的 nameErrorMessage
。程式碼如下:
describe('insuredAgeChange', () => {
let insured: Insured;
beforeEach(() => {
insured = {
name: '',
gender: '',
age: '',
nameErrorMessage: '',
ageErrorMessage: ''
};
});
it('should assign the value of the formControl to property "age" of the insured', () => {
// Arrange
const age = '18';
const errors = null;
// Act
component.insuredAgeChange(age, errors, insured);
// Assert
expect(insured.age).toBe(age);
});
it('should assign error message "此欄位必填" to property "ageErrorMessage" of the insured when the value of the formControl is empty string', () => {
// Arrange
const age = '';
const errors = { required: true };
const errorMessage = '此欄位必填';
// Act
component.insuredAgeChange(age, errors, insured);
// Assert
expect(insured.ageErrorMessage).toBe(errorMessage);
});
});
測試結果:
這個單元的測試也是相當簡單,基本上只要驗證執行後會新增一個被保人表單的資料即可。
程式碼如下:
describe('addInsured', () => {
it('should add a new insured data into property "insuredList" after being triggered', () => {
// Arrange
const expectResult: Insured[] = [{
name: '',
gender: '',
age: '',
nameErrorMessage: '',
ageErrorMessage: ''
}];
// Act
component.addInsured();
// Assert
expect(component.insuredList).toEqual(expectResult);
});
});
測試結果:
雖然很間單,但大家有沒有注意到,在這我裡我不是用 toBe
而是用 toEqual
來驗證?
toBe
的比較一般會用在原始型別的對比上,但如果今天要對比的是物件就要改成用 toEqual
來驗證了。
如果不知道為什麼的朋友,可能要先複習一下 JS 的核心概念囉!
參考文件:MDN 官方文件 - 理解相等比較模型。
測試結果:
最後一個單元也非常簡單,基本上只要驗證能將被保人的資料從 insuredList
中刪除即可。
程式碼如下
describe('deleteInsured', () => {
it('should delete the insured data by the index after being triggered', () => {
// Arrange
component.insuredList = [{
name: '',
gender: '',
age: '',
nameErrorMessage: '',
ageErrorMessage: ''
}];
// Act
component.deleteInsured(0);
// Assert
expect(component.insuredList).toEqual([]);
});
});
測試結果:
今天所有的測試結果:
不知道大家有沒有覺得今天的單元測試很簡單,甚至是有點無聊了呢?
與一開始還沒接觸時相比,是不是覺得其實寫單元測試也沒花多少時間,而且更可以保證程式碼的品質呢?
沒錯,這一切都會隨著熟練度的提升而變得愈加容易!
今天的學習重點主要是:
toBe
與 toEqual
的差別。今天的程式碼會放在 Github - Branch: day12 上供大家參考,建議大家在看我的實作之前,先按照需求規格自己做一遍,之後再跟我的對照,看看自己的實作跟我的實作不同的地方在哪裡、有什麼好處與壞處,如此反覆咀嚼消化後,我相信你一定可以進步地非常快!
如果有任何的問題或是回饋,也都非常歡迎留言給我讓我知道噢!