以整合測試要驗證的項目來說,這邊其實可以驗在 ngOnInit 被呼叫時, formBuilder 的 group 函式有沒有被呼叫,像是這樣:
1 2 3 4 5 6 7 8 9
it('should call function "group" of the "FormBuilder" when function "ngOnInit" be trigger', () => { // Arrange const formBuilder = TestBed.inject(FormBuilder); spyOn(formBuilder, 'group'); // Act fixture.detectChanges(); // Assert expect(formBuilder.group).toHaveBeenCalled(); });
it('should have attribute "type" and the value is "submit"', () => { // Arrange const attributeName = 'type'; const attributeValue = 'submit'; // Assert expect(buttonElement.getAttribute(attributeName)).toBe(attributeValue); });
it('should have attribute "disabled" when the form\'s status is invalid', () => { // Arrange const attributeName = 'disabled'; // Assert expect(buttonElement.hasAttribute(attributeName)).toBe(true); });
describe('When the form\'s status is valid', () => { beforeEach(() => { component.formGroup?.setValue({ account: '[email protected]', password: '12345678' }); fixture.detectChanges(); });
it('should not have attribute "disabled"', () => { // Arrange const attributeName = 'disabled'; // Assert expect(buttonElement.hasAttribute(attributeName)).toBe(false); });
it('should trigger function "login" when being clicked', () => { // Arrange spyOn(component, 'login'); // Act buttonElement.click(); // Assert expect(component.login).toHaveBeenCalled(); }); }); });
測試結果:
這次沒有任何預期外的狀況,不像上次剛好遇到奇怪的問題,搞不好這又是 Reactive Forms 的另一個優點呢!(笑)。
至此,我們已經完成了第一個里程碑:用 Template Driven Forms 的方式與用 Reactive Forms 的方式各自實作一個登入系統,並且也都為它們寫了單元測試以及整合測試,相信大家對於如何使用 Angular 製作表單與撰寫測試都有了長足的進步。
明天開始就要邁入下一個里程碑:用 Template Driven Forms 的方式與用 Reactive Forms 的方式各自實作一個動態的表單,並且也要都為它們寫單元測試以及整合測試,敬請期待(壞笑)。
describe('getErrorMessage', () => { it('should get empty string when the value is correct', () => { // Arrange const formControl = new FormControl(''); const expectedMessage = ''; // Act const message = component.getErrorMessage(formControl); // Assert expect(message).toBe(expectedMessage); });
it('should get empty string when the value is empty string but the form control is pristine', () => { // Arrange const formControl = new FormControl('', [Validators.required]); const expectedMessage = ''; // Act const message = component.getErrorMessage(formControl); // Assert expect(message).toBe(expectedMessage); });
it('should get "此欄位必填" when the value is empty string but the form control', () => { // Arrange const formControl = new FormControl('', [Validators.required]); const expectedMessage = '此欄位必填'; // Act formControl.markAsDirty(); const message = component.getErrorMessage(formControl); // Assert expect(message).toBe(expectedMessage); });
it('should get "格式有誤,請重新輸入" when the value is empty string but the form control', () => { // Arrange const formControl = new FormControl('whatever', [Validators.pattern('/^\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b$/gi')]); const expectedMessage = '格式有誤,請重新輸入'; // Act formControl.markAsDirty(); const message = component.getErrorMessage(formControl); // Assert expect(message).toBe(expectedMessage); });
it('should get "密碼長度最短不得低於8碼" when the value is empty string but the form control', () => { // Arrange const formControl = new FormControl('abc', [Validators.minLength(8)]); const expectedMessage = '密碼長度最短不得低於8碼'; // Act formControl.markAsDirty(); const message = component.getErrorMessage(formControl); // Assert expect(message).toBe(expectedMessage); });
it('should get "密碼長度最長不得超過16碼" when the value is empty string but the form control', () => { // Arrange const formControl = new FormControl('12345678901234567', [Validators.maxLength(16)]); const expectedMessage = '密碼長度最長不得超過16碼'; // Act formControl.markAsDirty(); const message = component.getErrorMessage(formControl); // Assert expect(message).toBe(expectedMessage); }); });
it('should have attribute "type" and the value is "submit"', () => { // Arrange const attributeName = 'type'; const attributeValue = 'submit'; // Assert expect(buttonElement.getAttribute(attributeName)).toBe(attributeValue); });
it('should have attribute "disabled" when the form\'s status is invalid', () => { // Arrange const attributeName = 'disabled'; // Assert expect(buttonElement.hasAttribute(attributeName)).toBe(true); });
describe('When the form\'s status is valid', () => { beforeEach(() => { component.account = '[email protected]'; component.password = '12345678'; fixture.detectChanges(); });
it('should not have attribute "disabled"', () => { // Arrange const attributeName = 'disabled'; // Assert expect(buttonElement.hasAttribute(attributeName)).toBe(false); });
it('should trigger function "login" when being clicked', () => { // Arrange spyOn(component, 'login'); // Act buttonElement.click(); // Assert expect(component.login).toHaveBeenCalled(); }); }); });
測試結果:
咦?怎麼會有 Error 咧?我自己在第一次遇到這個狀況也是有點傻眼,於是我深入調查了之後發現:
原來是因為 Karma 渲染出來的元素跟 Angular 渲染出來的元素狀態不一樣,Karma 渲染出來的 form 元素跟沒有正確吃到底下的表單欄位:
關於這個問題,我會再發 issue 詢問官方,如果後續有任何消息,我會再更新此篇文章讓大家知道。
至於目前這個案例,我們可以先在 it 的前面加上一個 x ,代表我們要 ignore 這個案例的意思,像這樣:
1 2 3 4 5 6
xit('should have attribute "disabled" when the form\'s status is invalid', () => { // Arrange const attributeName = 'disabled'; // Assert expect(buttonElement.hasAttribute(attributeName)).toBe(true); });
it('should assign the value to property "account"', () => { const accountControl = new FormControl('[email protected]'); component.accountValueChange(accountControl); expect(component.account).toBe(accountControl.value); });
乍看之下其實沒什麼太大的問題,也不是很難的程式碼,但如果這樣寫會更好一點:
1 2 3 4 5 6
it('should assign the value to property "account"', () => { const account = '[email protected]'; const accountControl = new FormControl(account); component.accountValueChange(accountControl); expect(component.account).toBe(account); });
又或者是這樣:
1 2 3 4 5 6
it('should assign the value to property "account"', () => { const accountControl = new FormControl('[email protected]'); component.accountValueChange(accountControl); const account = accountControl.value; expect(component.account).toBe(account); });
簡單來說就是一步一步來,將動作跟驗證分開,減少一些閱讀時的負擔,會讓整個程式碼更好閱讀。
此外,在撰寫測試時,有個 3A 原則的方式非常推薦大家使用。
3A 原則
這是在測試的世界裡,非常著名的方法。可以說是只要照著這個方法寫,滿簡單就能寫出不錯的測試。
而這個 3A 分別指的是:
Arrange - 準備物件或者是進行必要的前置作業。
Act - 實際執行、操作物件。
Assert - 進行結果驗證
以上面的程式碼為例, 3A 是這樣分的:
1 2 3 4 5 6 7 8 9
it('should assign the value to property "account"', () => { // Arrange const account = '[email protected]'; const accountControl = new FormControl(account); // Act component.accountValueChange(accountControl); // Assert expect(component.account).toBe(account); });
比較特別需要注意的就是當要開始執行 test case - 3 之前,會先執行的是 Test Suite - 2 的 beforeAll 。原因就像上面提過的:「在開始測試某測試集合裡面的測試案例之前,會先執行該測試集合的 beforeAll 」, test case - 3 是 Test Suite - 2 裡面的測試案例,所以在開始測試 test case - 3 之前,自然會先執行該測試集合裡的 beforeAll ,接著是父層測試集合裡的 beforeEach ,才會輪到 Test Suite - 2 裡面的 beforeEach 。
這樣的開發流程當然沒什麼太大的問題,不過俗話說得好:「人非聖賢,孰能無過。」,我們自己在測試時,非常容易就會因為各種無心、有心的關係,漏掉一些測試的案例;又或者跟別人開發時,我們不能保證別人都跟我們一樣在寫完程式之後都會乖乖測試,所以常常會造成改 A 壞 B ,甚至會有不符合需求的情況。
那如果我們將測試的步驟交給電腦來幫我們做會怎麼樣?
我的程式啟蒙老師說過一句話:「電腦很聽話,你讓它往東它不會往西,如果程式碼有錯就一定是你的錯,不會是電腦的錯」,所以如果把測試這件事情讓電腦來做,你有幾個案例它就會測幾個案例,你要它測幾遍他就測幾遍,而且執行起來的速度比我們手動還要快太多太多,一旦有錯馬上就會知道,如此一來,就不會發生改 A 壞 B 的情況,使我們的程式碼品質變得更好。