端對端測試是從使用者的角度進撰寫,因而會在不同的案例中重覆撰寫相同的程式碼,今天就來說明在 Cypress 如何降低重覆程式碼的比例。
在登入作業的測試程式內,帳號不存在、登入成功與失敗三個案例,我們都會輸入帳號與密碼,並且點選登入按鈕。所以在這些案例中會重覆撰寫取得頁面元素以及輸入與點選的動作。
it('當輸入不存在帳號, 應顯示錯誤訊息, 且無法按下登入按鈕', () => {
cy.fixture('users/id-not-exists').then(({ id, password }) => {
cy.get('button.mat-icon-button').contains('login').click();
cy.get('input[type=text]').type(id).blur();
cy.get('button').contains('登入').parent().should('be.disabled');
cy.get('mat-error').should('exist').and('have.text', '此帳號不存在');
});
});
it('當輸入錯誤帳號密碼, 應登入失敗', () => {
cy.fixture('users/login-failed').then(({ id, password }) => {
cy.get('button.mat-icon-button').contains('login').click();
cy.get('input[type=text]').type(id);
cy.get('input[type=password]').type(password);
cy.get('button').contains('登入').parent().click({ force: true }).should('not.be.disabled');
cy.get('.mat-snack-bar-container').should('exist').and('have.text', '登入失敗');
});
});
it('當輸入正確帳號密碼, 應登入成功', () => {
cy.fixture('users/login-success').then(({ id, password }) => {
cy.get('button.mat-icon-button').contains('login').click();
cy.get('input[type=text]').type(id);
cy.get('input[type=password]').type(password);
cy.get('button').contains('登入').parent().click({ force: true }).should('not.be.disabled');
cy.get('.mat-snack-bar-container').should('exist').and('have.text', '登入成功');
});
});
此時,可以利用 Cypress 所提供的 as()
方法,在取得頁面元素後給予一個別名。
describe('登入作業', () => {
beforeEach(() => {
cy.get('input[type=text]').as('id');
cy.get('input[type=password]').as('password');
cy.get('button').contains('登入').parent().as('loginButton');
});
it('當輸入不存在帳號, 應顯示錯誤訊息, 且無法按下登入按鈕', () => {
cy.fixture('users/id-not-exists').then(({ id, password }) => {
cy.get('@id').type(id).blur();
cy.get('@loginButton').should('be.disabled');
cy.get('mat-error').should('exist').and('have.text', '此帳號不存在');
});
});
it('當輸入錯誤帳號密碼, 應登入失敗', () => {
cy.fixture('users/login-failed').then(({ id, password }) => {
cy.get('@id').type(id);
cy.get('@password').type(password);
cy.get('@loginButton').click({ force: true }).should('not.be.disabled');
cy.get('.mat-snack-bar-container').should('exist').and('have.text', '登入失敗');
});
});
it('當輸入正確帳號密碼, 應登入成功', () => {
cy.fixture('users/login-success').then(({ id, password }) => {
cy.get('@id').type(id);
cy.get('@password').type(password);
cy.get('@loginButton').click({ force: true }).should('not.be.disabled');
cy.get('.mat-snack-bar-container').should('exist').and('have.text', '登入成功');
});
});
});
如上面程式所示,在 beforeEach
生命週期方法下,我們可以將帳號、密碼與登入按鈕的元素取得,然後利用 as()
設定一個別名。如此一來,就可以在各測試案例下,透過 @別名
的方式直接取得對應的頁面元素,來讓頁面元素的統一在 beforeEach
定義。
先前文章中,我們提到了在 Jasmine 中會利用 Page 物件來整理測試中取得頁面的元素,此一做法在 Cypress 也是適用的。不過,Cypress 建議使用自訂命令的方式來替代 Page 物件。透過自訂命令,我們可以自己把測試的使用者操作流程自定成一個命令,來在不同的測試案例中使用。
在開始說明自訂命令之前,需要針對應用程式專案做兩個設定。首先,當我們初始化 Cypress 之後,在 cypress
資料夾有一個 support
目錄,用來放我們所自訂的命令檔案。這篇所實作的自訂命令會寫在 commands.ts
檔案內,為了讓 Cypress 可以載入到自訂命令,我們需要在 support/e2e.ts
檔案中匯入 commands.ts
。
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import './commands';
另外,在 Cypress 的組態檔中,supportFile
參數用來設定在取得測試檔案前,要被載入的檔案路徑,預設 cypress/support/e2e.{js,jsx,ts,tsx}
;當這個參數值設定為 false
時,代表 Cypress 不載入其他的檔案。
設定完 Cypress 之後,就可以來建立一個自訂命令。如上面所說明,我們在登入的三個測試案例中,都指定了輸入完帳號與密碼後,去點選登入按鈕,以及驗證結果的訊息。此時,我們就可以自訂 inputIdAndPassword
、login
與 snackBarShouldBe
三個命令來簡化測試案例的內容。
首先,我們在 commands.ts
裡,去針對 Cypress.Chainable<Subject>
介面新增 inputIdAndPassword
、login
與 snackBarShouldBe
三個命令簽章:
declare namespace Cypress {
interface Chainable<Subject = any> {
inputIdAndPassword(id: string, password: string): Chainable<JQuery<HTMLElement>>;
login(): Chainable<JQuery<HTMLElement>>;
snackBarShouldBe(message: string): Chainable<JQuery<HTMLElement>>;
}
}
接著,就透過 Cypress.Commands
內的 add()
方法來新增自訂命令。這個方法會傳入命令名稱,以及命令方法主體兩個參數。
Cypress.Commands.add('inputIdAndPassword', (id: string, password: string) => {
cy.get('input[type=text]').type(id).blur();
if (password) {
cy.get('input[type=password]').type(password);
}
});
Cypress.Commands.add('login', () => {
return cy.get('button').contains('登入').parent().click({ force: true });
});
Cypress.Commands.add('snackBarShouldBe', (message: string) =>
cy.get('.mat-snack-bar-container').should('exist').and('have.text', message)
);
最後,我們就可以在測試案例中,直接使用自訂的 inputIdAndPassword
、login
與 snackBarShouldBe
三個命令。
describe('登入作業', () => {
it('當輸入不存在帳號, 應顯示錯誤訊息, 且無法按下登入按鈕', () => {
cy.fixture('users/id-not-exists').then(({ id, password }) => {
cy.inputIdAndPassword(id, password);
cy.login().should('be.disabled');
cy.get('mat-error').should('exist').and('have.text', '此帳號不存在');
});
});
it('當輸入錯誤帳號密碼, 應登入失敗', () => {
cy.fixture('users/login-failed').then(({ id, password }) => {
cy.inputIdAndPassword(id, password);
cy.login().should('not.be.disabled');
cy.snackBarShouldBe('登入失敗');
});
});
it('當輸入正確帳號密碼, 應登入成功', () => {
cy.fixture('users/login-success').then(({ id, password }) => {
cy.inputIdAndPassword(id, password);
cy.login().should('not.be.disabled');
cy.snackBarShouldBe('登入成功');
});
});
});
今天說明了 Cypress 提供的 as()
方法與自訂命令,完整的測試程式可以參考 GitHub。接下來,會來說明在執行完 Cypress 端對端測試後如何產生報表。