大家如果對於昨天的 E2E 測試如果沒有什麼問題的話,今天就來為我們的被保人表單撰寫 E2E 測試吧!
撰寫測試前的準備昨天有說過了,今天就不再贅述囉!不知道該幹嘛的朋友可以參考昨天實作開始的一開始的做了些什麼事情。
首先我們一樣先建立一個測試檔 insured-form.spec.js
,然後打開剛建立的測試檔加上此句語法讓編輯器可以知道我們在寫 Cypress 以方便撰寫測試程式碼:
/// <reference types="cypress" />
原理昨天一樣有介紹過了,忘記或不知道的朋友可以複習一下昨天的文章。
接著我們打開剛建立的測試檔,來寫我們的第一個 E2E 測試的測試案例,以驗證我們的環境已準備好。
程式碼如下:
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);
});
});
執行結果:
還記得之前在介紹 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 的初次見面(下) 有說明過,不知道的朋友可以回去複習一下。
藉由第一個測試案例來驗證環境沒問題後,我們就可以正式來寫需求的測試案例了。
複習並整理一下要驗的案例:
程式碼如下:
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');
});
});
執行結果:
大家有覺得昨天寫過一次後,今天再寫一次有比較熟悉一點了嗎?
雖然這次驗的情境比較多,但我覺得如果大多的情境都已經有被整合測試覆蓋到的話,或許只需要驗證第一個情境就好。
不過在現實中,寫整合測試的人不一定跟寫 E2E 測試的人是同一個,所以寫 E2E 的人照著需求規格寫,多驗一點情境也是很好的。
在今天的測試程式碼中,比較值得一提的是使用 cy.select()
的使用,它的參數可以欲選擇選項的 value
值,或者是選項的名稱,更可以是選項的 index
,是非常方便的一個 Command 。
此外,在選性別時,如果大家不是跟我一樣是點擊 Label
,而是直接點選 Radio Button 的話,記得要使用 cy.check()
的 Command。
不過就算寫錯也無所謂,因為 Cypress 這個貼心鬼其實都會跟你說你哪裡寫錯、可以怎麼寫。
例如剛剛說的 cy.select()
,如果我們使用 cy.click()
, Cypress 就會跟你說你可以用 cy.select()
來替代唷!而且還會跟你說你寫錯的地方是在哪一行:
又或者你使用了 cy.select()
,但忘記帶參數,它也會跟你說你漏了什麼參數:
Cypress 真是個貼心鬼
撰寫了兩次的 E2E 測試之後,也累積了不少測試案例,這時候大家應該會發現有一些重複的東西散落在不同的測試檔案之中,又或者會有某些 Hard Code 在測試程式碼裡的東西應該要被抽出來,以利後續維護。
這時我們就可以善用在第 18 天的文章裡曾經提過 fixtures
與 Cypress 的 cypress.json
的配置來達成。
舉例來說,如果你的 E2E 的測試專案都是在測同一個網域的網頁,那我們就可以在 cypress.json
加上 baseUrl
的設置:
{
"baseUrl": "http://localhost:4200"
}
如此就可讓我們後續使用 cy.visit()
、 cy.request()
或是 cy.intercept()
時,就可以不用再傳入一樣的字串。
而且這個用法還會有一個好處,就是當需要執行不同環境的測試時,我們可以用像是這樣子的方式來替換掉該變數:
$ CYPRESS_BASE_URL=https://product.domain.com cypress run
更多的環境變數小技巧請詳閱官方的 Environment Variables 文件。
上述提到的環境變數一般常用在會因為測試環境改變時需要改變的值上,但其實還有很多值是不會因為環境改變而改變的,這時就可以用上現在這個小技巧。
這個小技巧其實我也有在第 18 天的文章 ─ 與 Cypress 的初次見面(上) 裡稍微提到過,就是我們可以在 /fixtures
的資料夾底下新增 .json
檔,然後我們可以將值放在裡面,需要的時候再從裡面拿。
像現在我們可以在 /fixtures
裡新增一個 insured-form.json
的檔案,然後內容大概會是這樣:
{
"title": "Reactive Forms 實作 ─ 被保險人",
"name": "Leo",
"gender": "male",
"age": "18"
}
然後在 insured-form.spec.js
就可以改成這樣:
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 上供大家參考,建議大家在看我的實作之前,先按照需求規格自己做一遍,之後再跟我的對照,看看自己的實作跟我的實作不同的地方在哪裡、有什麼好處與壞處,如此反覆咀嚼消化後,我相信你一定可以進步地非常快!
如果你有任何的問題或是回饋,還請麻煩留言給我讓我知道!
Hi Leo,
此外,在選年齡時,如果大家不是跟我一樣是點擊 Label ,而是直接點選 Radio Button 的話,記得要使用 cy.check() 的 Command。
這句應該是在說性別不是年齡
另外透過Cypress的視窗一步一步去檢視每一步所發生的動作時,發現select age的部分他不會把結果呈現出來(如圖)不過如果直接執行整個測試就會出現
示意圖:
Hi ryan851109,
這句應該是在說性別不是年齡
感謝糾正!已更新文章內容。
另外透過Cypress的視窗一步一步去檢視每一步所發生的動作時,發現select age的部分他不會把結果呈現出來(如圖)不過如果直接執行整個測試就會出現
咦!照理來說是會的,如果不會可能是我哪邊有點問題才會這樣~"~
好的~我也再努力看看,說不定是我有哪裡出錯XD