Cypress - 自動化測試
Cypress
是一套可以執行 E2E、整合測試、單元測試的 Web 自動化測試工具。
功能特色:
- 支援多種瀏覽器測試
- 支援即時更新,撰寫測試時可以即時看到測試結果
- 支援互動性測試,例如:可以模擬用戶點擊、輸入等操作
環境建立
// 安裝 cypress
npm install cypress --save-dev
// 啟動 cypress 介面
npx cypress open
撰寫測試
測試通常包含三種階段
- 設定應用程式狀態(Setup): 設定測試的初始狀態
- 採取行動(Action): 模擬用戶的互動,對應用程式執行特定的操作
- 對應用程式狀態結果做出斷言(Assert):驗證應用程式的行為是否符合預期
describe
: 用於定義一個測試主題,底下會包含相關測試案例beforeEach
: 設定測試的初始狀態it
: 用於定義單獨的測試案例。裡面的 callback 定義測試的內容should
: 斷言,用於定義預測結果
describe 和 it 來自 Cypress 綁定的 Mocha 函式庫 expect 來自 Chai
簡單的範例如下:
describe("example to-do app", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/todo");
});
it("displays two todo items by default", () => {
// cy.get()為選擇器
cy.get(".todo-list li").should("have.length", 2);
cy.get(".todo-list li").first().should("have.text", "Pay electric bill");
cy.get(".todo-list li").last().should("have.text", "Walk the dog");
});
// context 可以將測試案例分組,以讓測試案例結構更清楚(ex:將新增單個/多個todo放在同一個context)
context("with a checked task", () => {
it("can add a new todo", () => {
const newItem = "Feed the cat";
//.type表示在輸入匡中輸入newItem內容,並模擬觸發enter鍵
cy.get("[data-test=new-todo]").type(`${newItem}{enter}`);
cy.get(".todo-list li")
.should("have.length", 3)
.last()
.should("have.text", newItem);
});
it("can add multiple new todos", () => {});
});
it("can check off an item as completed", () => {
// 除了cy.get之外,也可以用contains 來或許有特定內容的元素
// 此時會抓到 label 標籤,所以要往一層父層,再找對應的input元素中type為checkbox的元素並勾選
cy.contains("Pay electric bill")
.parent()
.find("input[type=checkbox]")
.check();
// 此時相對應的 li 上有 completed 的 css class
cy.contains("Pay electric bill")
.parents("li")
.should("have.class", "completed");
});
});
使用別名 Alias: as()
Cypress 可以將選擇的元素指定一個自定義的別名,以提高測試的可讀性。若在 beforeEach 中定義,則此命名可在不同測試案例中共用。
it("can check and uncheck items", () => {
beforeEach(() => {
// 選擇所有的待辦事項並命名為todos
cy.get(".todo-list li").as("todos");
});
// 使用自定義別名來進行操作和斷言
cy.get("@todos").first().find("input[type=checkbox]").check();
cy.get("@todos").eq(1).find("input[type=checkbox]").uncheck();
});
檢查路徑
可以使用 cy.location()
驗證目前所在的路徑和相關資訊,包括:hash、host、hostname、href、origin
、pathname、port、protocol、search 等
it('should navigate to "/sign-in" when you click the "Sign In" button', () => {
cy.get('[data-test="sign-in"]').click();
cy.location("pathname").should("contain", "sign-in");
//也可以寫成下面程式碼
cy.location().should((loc) => {
expect(loc.pathname).to.include("sign-in");
});
});
Task
Task 可以執行自定義的非同步操作,例如:操作 database 、模擬後端 API 回傳等。首先,要先 plugins 中進行定義。
// reset 為重設資料庫的非同步函式
import reset from "../../prisma/reset.cjs";
const plugins: Cypress.PluginConfig = (on) => {
on("task", {
reset() {
return reset;
},
});
};
export default plugins;
定義完之後就可以在測試中使用
// 在執行測試之前都會執行 reset
beforeEach(() => cy.task("reset"));
Commands 自定義命令
可以將重複性的操作定義成 command 如下
Cypress.Commands.add("signIn", (email, password) => {
cy.get('[data-test="sign-in-email"]').type(user.email);
cy.get('[data-test="sign-in-password"]').type(user.password);
cy.get('[data-test="sign-in-submit"]').click();
});
如此,就可以這樣使用
cy.signIn({ email: "steve@example.com", password: "password" });
也很適合用作獲取資料屬性
Cypress.Commands.add("getData", (attribute) => {
return cy.get(`[data-test="${attribute}"]`);
});
//在測試案例中使用
cy.getData("sign-in-email").type(user.email);
API 請求
常見有 2 種方式處理
- 使用真實的 API: 直接向後端 server 發送請求,並處理真正的數據
- 使用 mock data: 測試攔截 API 請求,並使用 mock data 來模擬後端的 response
cy.intercept
用來模擬和控制瀏覽器對 API 請求的處理,可以攔截和修改請求。
舉例來說,下面是 mock search API 的請求
//自定義 getSearch command
//攔截 /search* 的 GET 請求,並設定 response body 的 mock data
cy.intercept("/search*", [{ item: "Book 1" }, { item: "Book 2" }]).as(
"getSearch"
);
cy.get('[data-testid="autocomplete"]').type("Book");
cy.wait("@getSearch");
cy.get('[data-testid="results"]')
.should("contain", "Book 1")
.and("contain", "Book 2");
更多關於Network requests
Cookies
cy.getCookie
和 cy.setCookie
用來讀寫 cookies。
// 將real API response 資料設至 cookies
// 如果在 beforeEach 中 設置 user token ,就不用每次都登入了
cy.request("/echo-chamber/api/users")
.then((response) => {
const [user] = response.body.users;
cy.setCookie("jwt", encodeToken(user)).then(() => user);
})
.as("user");
撰寫測試的原則
1. 保持簡單 Keep it simple
只測試單一功能或行為,避免在一個測試案例中包含過多的測試目標
2. 獨立性 Independence
每個測試案例應該是相互獨立的,不應該依賴其他測試的結果
3. 易讀性 readability and clarity
測試案例的名稱和描述應簡潔易懂,使用有意義的變數名稱和註釋,避免過多的巢狀結構
4. 最小斷言 One Assertion per test
每個測試案例應該包含一個主要斷言,用於驗證測試單一目標
5. 極端條件 Edge cases
測試應包含各種極端狀況,包括:極端值、異常情況和無效輸入,確保程式在不同情況下都能正常運作
6. 初始化和清理 Setup and Teardown
確保測試的初始狀態和結束狀態(清理)都是已知的,且狀態不會互相干擾
7. 持續更新 Continuous Maintenance
測試案例應隨著程式碼變更而持續更新維護
8. 可擴展性 Scalability
考量未來程式碼可能的變化,避免過多的 hard code,讓測試易於調整和擴展