Angular 深入淺出三十天:表單與測試 Day12 - 單元測試實作 - 被保人 by Template Driven Forms

Day12

今天我們要來為我們用 Template Driven Forms 所撰寫的被保人表單寫單元測試,如果還沒有相關程式碼的朋友,趕快前往閱讀第十天的文章: Template Driven Forms 實作 - 動態表單初體驗

此外,由於許多同樣的事情已在第六天的文章:單元測試實作 - 登入系統 by Template Driven Forms 講過了,例如前置作業的部份,我就不再重複贅述囉!

實作開始

個人習慣要撰寫測試時的第一件事情,就是先把目標類別的依賴都先準備好,例如我們的被保人表單至少會需要 FormsModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 來看一下要寫哪些案例:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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;
}
}

以目前的程式碼來看,我們要測的單元有 trackByIndexinsuredNameChangeinsuredAgeChangeaddInsureddeleteInsured 這五個,接下來我們照順序先從 trackByIndex 來寫好了。

測試單元 - trackByIndex

這個測試單元非常簡單,不多說直接看程式碼:

1
2
3
4
5
6
7
8
describe('trackByIndex', () => {
it('should just return the index', () => {
// Arrange
const index = 0;
// Assert
expect(component.trackByIndex(index)).toBe(index);
})
});

測試結果:

testing result

測試單元 - insuredNameChange

接下來要測的單元是 insuredNameChange ,要測的案例有:

  1. 會將傳入的 name 的值賦值給傳入的 insured 裡的 name
  2. 如果傳入的 errorsrequired 欄位,則會將錯誤訊息 此欄位必填 賦值給傳入的 insured 裡的 nameErrorMessage
  3. 如果傳入的 errorsminlength 欄位,則會將錯誤訊息 姓名至少需兩個字以上 賦值傳入的 insured 裡的 nameErrorMessage

程式碼如下:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 裡,讓每個測試案例在執行之前都能拿到重新初始化過後的值,避免與其他的測試案例共用同個資料或物件。

測試結果:

testing result

測試單元 - insuredAgeChange

下個要測的單元是 insuredAgeChange ,基本上跟 insuredNameChange 相似度高達 87% ,要測試的案例有:

  1. 會將傳入的 age 的值賦值給傳入的 insured 裡的 name
  2. 如果傳入的 errorsrequired 欄位,則會將錯誤訊息 此欄位必填 賦值給傳入的 insured 裡的 nameErrorMessage

程式碼如下:

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
34
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);
});
});

測試結果:

testing result

測試單元 - addInsured

這個單元的測試也是相當簡單,基本上只要驗證執行後會新增一個被保人表單的資料即可。

程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
});
});

測試結果:

testing result

雖然很間單,但大家有沒有注意到,在這我裡我不是用 toBe 而是用 toEqual 來驗證?

toBe 的比較一般會用在原始型別的對比上,但如果今天要對比的是物件就要改成用 toEqual 來驗證了。

如果不知道為什麼的朋友,可能要先複習一下 JS 的核心概念囉!

參考文件:MDN 官方文件 - 理解相等比較模型

測試結果:

testing result

測試單元 - deleteInsured

最後一個單元也非常簡單,基本上只要驗證能將被保人的資料從 insuredList 中刪除即可。

程式碼如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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([]);
});
});

測試結果:

testing result

今天所有的測試結果:

testing result

本日小結

不知道大家有沒有覺得今天的單元測試很簡單,甚至是有點無聊了呢?

與一開始還沒接觸時相比,是不是覺得其實寫單元測試也沒花多少時間,而且更可以保證程式碼的品質呢?

沒錯,這一切都會隨著熟練度的提升而變得愈加容易!

今天的學習重點主要是:

  1. 清楚 toBetoEqual 的差別。
  2. 測試隔離
  3. 練習寫測試

今天的程式碼會放在 Github - Branch: day12 上供大家參考,建議大家在看我的實作之前,先按照需求規格自己做一遍,之後再跟我的對照,看看自己的實作跟我的實作不同的地方在哪裡、有什麼好處與壞處,如此反覆咀嚼消化後,我相信你一定可以進步地非常快!

如果有任何的問題或是回饋,也都非常歡迎留言給我讓我知道噢!

評論

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×