昨天幫我們用 Reactive Forms 所撰寫的被保人表單寫完單元測試之後,今天則是要來為它寫整合測試。
大家還記得整合測試的目標是要測什麼嗎?我幫大家複習一下:
整合測試的測試目標是要測是兩個 或是兩個以上 的類別之間的互動 是否符合我們的預期。
實作開始 首先我們先增加一個 Integration testing
的區塊,有關於整合測試的程式碼接下來都會放在這裡面,至於昨天的就放在 Unit testing
的區塊:
1 2 3 4 5 6 7 8 9 10 11 describe('TemplateDrivenFormsAsyncInsuredComponent' , () => { describe('Unit testing' , () => { }); describe('Integration testing' , () => { }); });
跟之前樣先打開 .html
來看一下目前的程式碼:
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 <form *ngIf ="formGroup" [formGroup ]="formGroup" (submit )="submit()" > <ng-container formArrayName ="insuredList" *ngFor ="let control of formArray.controls; let index = index" > <fieldset [formGroupName ]="index" > <legend > 被保人</legend > <p > <label [for ]="'name-' + index" > 姓名:</label > <input type ="text" [id ]="'name-' + index" formControlName ="name" /> <span class ="error-message" > {{ getErrorMessage("name", index) }}</span > </p > <p > 性別: <input type ="radio" [id ]="'male-' + index" value ="male" formControlName ="gender" /> <label [for ]="'male-' + index" > 男</label > <input type ="radio" [id ]="'female-' + index" value ="female" formControlName ="gender" /> <label [for ]="'female-' + index" > 女</label > </p > <p > <label [for ]="'age-' + index" > 年齡:</label > <select name ="age" [id ]="'age-' + index" formControlName ="age" > <option value ="" > 請選擇</option > <option value ="18" > 18歲</option > <option value ="20" > 20歲</option > <option value ="70" > 70歲</option > <option value ="75" > 75歲</option > </select > <span class ="error-message" > {{ getErrorMessage("age", index) }}</span > </p > <p > <button type ="button" (click )="deleteInsured(index)" > 刪除</button > </p > </fieldset > </ng-container > <p > <button type ="button" (click )="addInsured()" > 新增被保險人</button > <button type ="submit" [disabled ]="isFormInvalid" > 送出</button > </p > </form >
大家有看出來要測什麼了嗎?我來幫大家整理一下要測的項目:
姓名欄位
屬性 type
的值要是 text
屬性 formControlName
的值要是 name
當此欄位的狀態是 pristine
時,則不會有錯誤訊息
當此欄位的狀態不是 pristine
且欄位的值為空字串時,則顯示 此欄位必填
的錯誤訊息
當此欄位的狀態不是 pristine
且欄位的值只有一個字時,則顯示 姓名至少需兩個字以上
的錯誤訊息
當此欄位的狀態不是 pristine
且欄位的值超過十個字時,則顯示 姓名至多只能輸入十個字
的錯誤訊息
性別欄位
男
屬性 type
的值要是 radio
屬性 value
的值要是 male
屬性 formControlName
的值要是 gender
女
屬性 type
的值要是 radio
屬性 value
的值要是 male
屬性 formControlName
的值要是 gender
年齡欄位
屬性 formControlName
的值要是 age
當此欄位的狀態是 pristine
時,則不會有錯誤訊息
當此欄位的狀態不是 pristine
且欄位的值為空字串時,則顯示 此欄位必填
的錯誤訊息
新增被保人按鈕
刪除被保人按鈕
送出按鈕
屬性 type
的值要是 submit
沒有任何被保人時,送出按鈕皆呈現不可被點選之狀態
任一個被保人的驗證有誤時,送出按鈕皆呈現不可被點選之狀態
當所有的被保人資料皆正確時,按下送出按鈕要能觸發函式 submit
把要測的項目都列出來之後,有沒有覺得要測的項目很多阿?哈哈!
再次跟大家說明,雖然上面這些項目有些其實並不真的屬於整合測試 的範圍,但我個人會在這時候一起測,因為這樣可以省下一些重複的程式碼。
大家應該還記得怎麼測吧?忘記的趕快回去看一下之前的文章!
此外,開始之前也別忘記先做以下程式碼所展示的前置作業,後面將不再贅述:
1 2 3 4 5 6 7 8 9 describe('Integration testing' , () => { let compiledComponent: HTMLElement; beforeEach(() => { compiledComponent = fixture.nativeElement; }); });
姓名欄位的驗證 複習一下姓名欄位的驗證項目:
屬性 type
的值要是 text
屬性 formControlName
的值要是 name
當此欄位的狀態是 pristine
時,則不會有錯誤訊息
當此欄位的狀態不是 pristine
且欄位的值為空字串時,則顯示 此欄位必填
的錯誤訊息
當此欄位的狀態不是 pristine
且欄位的值只有一個字時,則顯示 `姓名至少
程式碼如下:
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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 describe('the insured fields' , () => { let formGroup: FormGroup; beforeEach(() => { const nameControl = new FormControl('' , [ Validators.required, Validators.minLength(2 ), Validators.maxLength(10 ) ]); const genderControl = new FormControl('' , Validators.required); const ageControl = new FormControl('' , Validators.required); formGroup = new FormGroup({ name: nameControl, gender: genderControl, age: ageControl }); component.formArray.push(formGroup); fixture.detectChanges(); }); describe('the name input field' , () => { let nameInputElement: HTMLInputElement; beforeEach(() => { nameInputElement = compiledComponent.querySelector('#name-0' )!; }); it('should have attribute "type" and the value is "text"' , () => { const attributeName = 'type' ; const attributeValue = 'text' ; expect(nameInputElement.getAttribute(attributeName)).toBe(attributeValue); }); it('should have attribute "formControlName" and the value is "name"' , () => { const attributeName = 'formControlName' ; const attributeValue = 'name' ; expect(nameInputElement.getAttribute(attributeName)).toBe(attributeValue); }); describe('Error Messages' , () => { let nameFormControl: FormControl; beforeEach(() => { nameFormControl = formGroup.get('name' ) as FormControl; }); it('should be empty string when property "pristine" of the "formControl" is `true`' , () => { const targetElement = compiledComponent.querySelector('#name-0 + .error-message' ); expect(targetElement?.textContent).toBe('' ); }); describe('when the field is dirty' , () => { beforeEach(() => { nameFormControl.markAsDirty(); fixture.detectChanges(); }); it('should be "此欄位必填" when the value is empty string' , () => { const errorMessage = '此欄位必填' ; const targetElement = compiledComponent.querySelector('#name-0 + .error-message' ); expect(targetElement?.textContent).toBe(errorMessage); }); it('should be "姓名至少需兩個字以上" when the value\'s length less than 2' , () => { nameFormControl.setValue('A' ) const errorMessage = '姓名至少需兩個字以上' ; const targetElement = compiledComponent.querySelector('#name-0 + .error-message' ); fixture.detectChanges(); expect(targetElement?.textContent).toBe(errorMessage); }); it('should be "姓名至多只能輸入十個字" when the value\'s length greater than 10' , () => { nameFormControl.setValue('ABCDE123456' ) const errorMessage = '姓名至多只能輸入十個字' ; const targetElement = compiledComponent.querySelector('#name-0 + .error-message' ); fixture.detectChanges(); expect(targetElement?.textContent).toBe(errorMessage); }); it('should be empty string when there are not any errors' , () => { nameFormControl.setValue('ABCDE123456' ) const errorMessage = '姓名至多只能輸入十個字' ; const targetElement = compiledComponent.querySelector('#name-0 + .error-message' ); fixture.detectChanges(); expect(targetElement?.textContent).toBe(errorMessage); }); }); }); }); });
測試結果:
這段程式碼中有兩個重點:
為了之後測其他欄位,我多新增了一個 test insured fields
的 describe
。這是因為要驗證這些欄位之前,一定要先讓被保人的表單長出來,所我才會多包一層,並把大家都會做的事情拉到這層的 beforeEach
來做。
切記不要使用 component.addInsured()
來新增被保人。
性別欄位的驗證 性別欄位要驗證的部份非常簡單,項目如下:
男
屬性 type
的值要是 radio
屬性 value
的值要是 male
屬性 formControlName
的值要是 gender
女
屬性 type
的值要是 radio
屬性 value
的值要是 male
屬性 formControlName
的值要是 gender
測試程式碼如下:
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 describe('the gender radio buttons' , () => { let radioButtonElement: HTMLInputElement; describe('male' , () => { beforeEach(() => { radioButtonElement = compiledComponent.querySelector(`#male-0` )!; }); it('should have attribute "type" and the value is "radio"' , () => { const attributeName = 'type' ; const attributeValue = 'radio' ; expect(radioButtonElement.getAttribute(attributeName)).toBe(attributeValue); }); it('should have attribute "formControlName" and the value is "gender"' , () => { const attributeName = 'formControlName' ; const attributeValue = 'gender' ; expect(radioButtonElement.getAttribute(attributeName)).toBe(attributeValue); }); it('should have attribute "value" and the value is "male"' , () => { const attributeName = 'value' ; const attributeValue = 'male' ; expect(radioButtonElement.getAttribute(attributeName)).toBe(attributeValue); }); }); describe('female' , () => { beforeEach(() => { radioButtonElement = compiledComponent.querySelector(`#female-0` )!; }); it('should have attribute "type" and the value is "radio"' , () => { const attributeName = 'type' ; const attributeValue = 'radio' ; expect(radioButtonElement.getAttribute(attributeName)).toBe(attributeValue); }); it('should have attribute "formControlName" and the value is "gender"' , () => { const attributeName = 'formControlName' ; const attributeValue = 'gender' ; expect(radioButtonElement.getAttribute(attributeName)).toBe(attributeValue); }); it('should have attribute "value" and the value is "female"' , () => { const attributeName = 'value' ; const attributeValue = 'female' ; expect(radioButtonElement.getAttribute(attributeName)).toBe(attributeValue); }); }); });
測試結果:
年齡欄位的驗證 年齡欄位要驗證的項目如下:
屬性 formControlName
的值要是 age
當此欄位的狀態是 pristine
時,則不會有錯誤訊息
當此欄位的狀態不是 pristine
且欄位的值為空字串時,則顯示 此欄位必填
的錯誤訊息
程式碼如下:
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 describe('the age field' , () => { const key = 'age-0' let ageSelectElement: HTMLSelectElement; beforeEach(() => { ageSelectElement = compiledComponent.querySelector(`#${key} ` )!; }); it('should have attribute "formControlName" and the value is "age"' , () => { const attributeName = 'formControlName' ; const attributeValue = 'age' ; expect(ageSelectElement.getAttribute(attributeName)).toBe(attributeValue); }); describe('Error Messages' , () => { let ageFormControl: FormControl; beforeEach(() => { ageFormControl = formGroup.get('age' ) as FormControl; }); it('should be empty string when property "pristine" of the "formControl" is `true`' , () => { const targetElement = compiledComponent.querySelector('#age-0 + .error-message' ); expect(targetElement?.textContent).toBe('' ); }); describe('when the field is dirty' , () => { beforeEach(() => { ageFormControl.markAsDirty(); fixture.detectChanges(); }); it('should be "此欄位必填" when the value is empty string' , () => { const errorMessage = '此欄位必填' ; const targetElement = compiledComponent.querySelector('#age-0 + .error-message' ); expect(targetElement?.textContent).toBe(errorMessage); }); }); }); });
年齡欄位的驗證跟姓名的驗證有 87% 像,複製過來再稍微調整一下即可。
測試結果:
刪除按鈕的驗證 刪除被保人按鈕要驗證的是:按下按鈕要能觸發函式 deleteInsured
。這部份大家只要使用 Spy
的技巧來驗證即可,也是頗為簡單。
程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 describe('Delete insured button' , () => { it('should trigger function `deleteInsured` after being clicked' , () => { const index = 0 ; const deleteButtonElement = compiledComponent.querySelector('fieldset button[type="button"]' ) as HTMLElement; spyOn(component, 'deleteInsured' ); deleteButtonElement.click(); expect(component.deleteInsured).toHaveBeenCalledWith(index); }); });
測試結果:
新增被保人按鈕的驗證 新增被保人按鈕要驗證的是:按下按鈕要能觸發函式 addInsured
,跟刪除被保人的按鈕要驗證的項目幾乎是一模一樣,複製過來稍微修改一下即可。
程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 describe('add insured button' , () => { it('should trigger function `addInsured` after being clicked' , () => { const addButtonElement = compiledComponent.querySelector('p:last-child button[type="button"]' ) as HTMLElement; spyOn(component, 'addInsured' ); addButtonElement.click(); expect(component.addInsured).toHaveBeenCalled(); }); });
測試結果:
送出按鈕的驗證 最後,送出按鈕要驗證的項目是:
屬性 type
的值要是 submit
沒有任何被保人時,送出按鈕皆呈現不可被點選之狀態
任一個被保人的驗證有誤時,送出按鈕皆呈現不可被點選之狀態
當所有的被保人資料皆正確時,按下送出按鈕要能觸發函式 submit
程式碼如下:
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 describe('submit button' , () => { let buttonElement: HTMLButtonElement; beforeEach(() => { buttonElement = compiledComponent.querySelector('button[type="submit"]' ) as HTMLButtonElement; }); it('should be existing' , () => { expect(buttonElement).toBeTruthy(); }); it('should be disabled when there are not any insureds' , () => { expect(buttonElement.hasAttribute('disabled' )).toBe(true ); }); describe('When there is a insured' , () => { let formGroup: FormGroup; beforeEach(() => { const nameControl = new FormControl('' , [ Validators.required, Validators.minLength(2 ), Validators.maxLength(10 ) ]); const genderControl = new FormControl('' , Validators.required); const ageControl = new FormControl('' , Validators.required); formGroup = new FormGroup({ name: nameControl, gender: genderControl, age: ageControl }); component.formArray.push(formGroup); fixture.detectChanges(); }); it('should be disabled when there ara any verifying errors that insured\'s data' , () => { compiledComponent.querySelector('button[type="submit"]' ) fixture.detectChanges(); expect(buttonElement.hasAttribute('disabled' )).toBe(true ); }) it('should be enabled when there ara any verifying errors that insured\'s data' , () => { formGroup.patchValue({ name: 'Leo' , gender: 'male' , age: '18' , }); fixture.detectChanges(); expect(buttonElement.hasAttribute('disabled' )).toBe(false ); }) }); });
測試結果:
至此,我們就完成了整合測試的部份囉!
今天所有的測試結果:
本日小結 今天一樣主要是讓大家練習,提昇撰寫測試的熟悉度,該講的重點應該在之前的文章都有提到。
不過我相信大家應該寫差不多類型的測試寫到有點索然無味了,所以我明天不會讓大家寫測試,而是會總結一下 Template Driven Forms 與 Reactive Forms 這兩種開發方式的優缺點,敬請期待。
今天的實作程式碼會放在 Github - Branch: day15 供大家參考,建議大家在看我的實作之前,先按照需求規格自己做一遍,之後再跟我的對照,看看自己的實作跟我的實作不同的地方在哪裡、有什麼好處與壞處,如此反覆咀嚼消化後,我相信你一定可以進步地非常快!
如果有任何的問題或是回饋,也都非常歡迎留言給我讓我知道噢!