Angular 深入淺出三十天:表單與測試 Day21 - E2E 測試實作 - 被保人表單

Day21

大家如果對於昨天的 E2E 測試如果沒有什麼問題的話,今天就來為我們的被保人表單撰寫 E2E 測試吧!

實作開始

撰寫測試前的準備昨天有說過了,今天就不再贅述囉!不知道該幹嘛的朋友可以參考昨天實作開始的一開始的做了些什麼事情。

首先我們一樣先建立一個測試檔 insured-form.spec.js,然後打開剛建立的測試檔加上此句語法讓編輯器可以知道我們在寫 Cypress 以方便撰寫測試程式碼:

1
/// <reference types="cypress" />

原理昨天一樣有介紹過了,忘記或不知道的朋友可以複習一下昨天的文章

被保人表單的第一個 E2E 測試的測試案例

接著我們打開剛建立的測試檔,來寫我們的第一個 E2E 測試的測試案例,以驗證我們的環境已準備好。

程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
describe('Insured Form', () => {
beforeEach(() => {
cy.visit('http://localhost:4200');
cy.get('ul li').contains(title).click();
});

it('have title "Reactive Forms 實作 ─ 被保險人"', () => {
// Arrange
const title = 'Reactive Forms 實作 ─ 被保險人';
// Assert
cy.get('h1').should('have.text', title);
});
});

執行結果:

Testing Result

還記得之前在介紹 Test Runner 的時候有稍稍帶過 contains 這個 Command 嗎?

確切是在第 19 天的文章: 與 Cypress 的初次見面(下)

這次特別使用一次給大家看,因為如果不使用這個方式, CSS Selector 可能就要寫成: cy.get('ul li:last-child > a').click(); ,滿醜的。

當然根據官方的 Best Practice ,直接在上面加個 data-cy="insured-form-page-link" 的屬性是最好的。

原因一樣在第 19 天的文章: 與 Cypress 的初次見面(下) 有說明過,不知道的朋友可以回去複習一下。

撰寫測試案例

藉由第一個測試案例來驗證環境沒問題後,我們就可以正式來寫需求的測試案例了。

複習並整理一下要驗的案例:

  • 要可以新增被保險人
  • 要可以刪除被保險人
  • 輸入正確姓名與選擇年齡後,但沒選擇性別,送出按鈕為 disabled 的狀態
  • 輸入正確姓名與選擇性別後,但沒選擇年齡,送出按鈕為 disabled 的狀態
  • 選擇性別與年齡後,但沒輸入姓名,送出按鈕為 disabled 的狀態

程式碼如下:

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
describe('Insured Form', () => {
beforeEach(() => {
cy.visit('http://localhost:4200');
cy.get('ul li').contains('Reactive Forms 實作 ─ 被保險人').click();
});

it('have title "Reactive Forms 實作 ─ 被保險人"', () => {
// Arrange
const title = 'Reactive Forms 實作 ─ 被保險人';
// Assert
cy.get('h1').should('have.text', title);
});

it('should can add the insured', () => {
// Arrange
const name = 'Leo';
const gender = 'male';
const age = '18';
// Act
cy.get('[type="button"]').click();
cy.get('#name-0').type(name);
cy.get(`[for="${gender}-0"]`).click();
cy.get('#age-0').select(age);
// Assert
cy.get('[type="submit"]').should('be.enabled');
});

it('should can delete the insured', () => {
// Act
cy.get('[type="button"]').click();
cy.get('fieldset').contains('刪除').click();
// Assert
cy.get('fieldset').should('have.length', 0);
});

it('should can not add the insured when the age is not valid', () => {
// Arrange
const name = 'Leo';
const gender = 'male';
// Act
cy.get('[type="button"]').click();
cy.get('#name-0').type(name);
cy.get(`[for="${gender}-0"]`).click();
// Assert
cy.get('[type="submit"]').should('be.disabled');
});

it('should can not add the insured when the gender is not valid', () => {
// Arrange
const name = 'Leo';
const age = '18';
// Act
cy.get('[type="button"]').click();
cy.get('#name-0').type(name);
cy.get('#age-0').select(age);
// Assert
cy.get('[type="submit"]').should('be.disabled');
});

it('should can not add the insured when the name is not valid', () => {
// Arrange
const gender = 'male';
const age = '18';
// Act
cy.get('[type="button"]').click();
cy.get(`[for="${gender}-0"]`).click();
cy.get('#age-0').select(age);
// Assert
cy.get('[type="submit"]').should('be.disabled');
});
});

執行結果:

Testing Result

大家有覺得昨天寫過一次後,今天再寫一次有比較熟悉一點了嗎?

雖然這次驗的情境比較多,但我覺得如果大多的情境都已經有被整合測試覆蓋到的話,或許只需要驗證第一個情境就好。

不過在現實中,寫整合測試的人不一定跟寫 E2E 測試的人是同一個,所以寫 E2E 的人照著需求規格寫,多驗一點情境也是很好的。

在今天的測試程式碼中,比較值得一提的是使用 cy.select() 的使用,它的參數可以欲選擇選項的 value 值,或者是選項的名稱,更可以是選項的 index ,是非常方便的一個 Command 。

此外,在選年齡時,如果大家不是跟我一樣是點擊 Label ,而是直接點選 Radio Button 的話,記得要使用 cy.check() 的 Command。

Cypress 的錯誤訊息

不過就算寫錯也無所謂,因為 Cypress 這個貼心鬼其實都會跟你說你哪裡寫錯、可以怎麼寫。

例如剛剛說的 cy.select() ,如果我們使用 cy.click() , Cypress 就會跟你說你可以用 cy.select() 來替代唷!而且還會跟你說你寫錯的地方是在哪一行:

Error Message

又或者你使用了 cy.select() ,但忘記帶參數,它也會跟你說你漏了什麼參數:

Error Message

Cypress 真是個貼心鬼

撰寫了兩次的 E2E 測試之後,也累積了不少測試案例,這時候大家應該會發現有一些重複的東西散落在不同的測試檔案之中,又或者會有某些 Hard Code 在測試程式碼裡的東西應該要被抽出來,以利後續維護。

這時我們就可以善用在第 18 天的文章裡曾經提過 fixtures 與 Cypress 的 cypress.json 的配置來達成。

E2E 測試小技巧 ─ 環境變數

舉例來說,如果你的 E2E 的測試專案都是在測同一個網域的網頁,那我們就可以在 cypress.json 加上 baseUrl 的設置:

1
2
3
{
"baseUrl": "http://localhost:4200"
}

如此就可讓我們後續使用 cy.visit()cy.request() 或是 cy.intercept() 時,就可以不用再傳入一樣的字串。

而且這個用法還會有一個好處,就是當需要執行不同環境的測試時,我們可以用像是這樣子的方式來替換掉該變數:

1
$ CYPRESS_BASE_URL=https://product.domain.com cypress run

更多的環境變數小技巧請詳閱官方的 Environment Variables 文件。

E2E 測試小技巧 ─ fixtures

上述提到的環境變數一般常用在會因為測試環境改變時需要改變的值上,但其實還有很多值是不會因為環境改變而改變的,這時就可以用上現在這個小技巧。

這個小技巧其實我也有在第 18 天的文章 ─ 與 Cypress 的初次見面(上) 裡稍微提到過,就是我們可以在 /fixtures 的資料夾底下新增 .json 檔,然後我們可以將值放在裡面,需要的時候再從裡面拿。

像現在我們可以在 /fixtures 裡新增一個 insured-form.json 的檔案,然後內容大概會是這樣:

1
2
3
4
5
6
{
"title": "Reactive Forms 實作 ─ 被保險人",
"name": "Leo",
"gender": "male",
"age": "18"
}

然後在 insured-form.spec.js 就可以改成這樣:

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
import insuredForm from '../fixtures/insured-form.json';

describe('Insured Form', () => {
beforeEach(() => {
cy.visit('');
cy.get('ul li').contains(insuredForm.title).click();
});

it('have title "Reactive Forms 實作 ─ 被保險人"', () => {
// Arrange
const title = insuredForm.title;
// Assert
cy.get('h1').should('have.text', title);
});

it('should can add the insured', () => {
// Arrange
const name = insuredForm.name;
const gender = insuredForm.gender;
const age = insuredForm.age;
// Act
cy.get('[type="button"]').click();
cy.get('#name-0').type(name);
cy.get(`[for="${gender}-0"]`).click();
cy.get('#age-0').select(age);
// Assert
cy.get('[type="submit"]').should('be.enabled');
});

// 以下省略...
});

如此一來,未來當驗證的資料需要改變時,就只要到 /fixtures 裡的 insured-form.json 改就好,維護起來就更加輕鬆愉快囉!

今天我故意沒有用自訂 Command 的技巧來重構我的測試程式碼,大家不妨試著自己自訂看看吧!

本日小結

今天的重點主要是後面的兩個小技巧,這兩個小技巧對於日後大家真的在自己的專案或為公司專案撰寫 E2E 測試會非常有幫助,請務必多加熟悉。

不過平常都用 TypeScript 寫的我覺得很不習慣,明天就來分享怎麼樣把它變成 TypeScript 的版本吧!

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

如果你有任何的問題或是回饋,還請麻煩留言給我讓我知道!

評論

Your browser is out-of-date!

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

×