上一篇在認識 Timeout 時提到 Hooks,Hooks 是測試框架裡非常常見的生命週期輔助方法,大部份的測試框架命名都大同小異,我們開始來認識 Playwright 的 Hooks 吧!
describe
:將所有關聯的測試集合起來beforeEach
:在「每一個測試開始前」執行afterEach
:在「每一個測試結束後」執行beforeAll
:在「同一個檔案內的所有測試前」,或「describe 區塊開始前」執行一次afterAll
:在「同一個檔案內的所有測試後」,或「describe 區塊結束後」執行一次為什麼測試流程需要 Hooks 呢?Hooks 可以幫助我們什麼?想像一下,現在我們要為一個網站撰寫多個測試,每次測試都需要相同的準備工作,例如:登入、點擊到特定頁面,測試結束後也要做相同的清理工作,例如:登出、關閉瀏覽器,那測試腳本會像是這個樣子:
// 第一個測試:找到第 1 隻貓咪
test('find the cat 1', async ({ page }) => {
// --- 重複的準備工作 【輸入帳號密碼、登入】---
await page.goto('/');
await page.locator('input[name="username"]').fill('ada');
await page.locator('input[name="password"]').fill('adaPassword');
await page.getByRole('button', { name: "Login" }).click();
await expect(page).toHaveURL(/playground/);
// --- 準備工作結束 ---
// --- 尋找目標 [Cat 1] ---
await page.getByRole('button', { name: "Cat Zone" }).click();
await expect(page.getByRole('img', { name: "first cat" })).toBeVisible();
// --- 重複的清理工作 【登出、關閉瀏覽器】---
await page.getByRole('button', { name: "Logout" }).click();
await page.close();
// --- 清理工作結束 ---
});
// 第二個測試:找到第 2 隻貓咪
test('find the cat 2', async ({ page }) => {
// --- 重複的準備工作 【輸入帳號密碼、登入】---
await page.goto('/');
await page.locator('input[name="username"]').fill('ada');
await page.locator('input[name="password"]').fill('adaPassword');
await page.getByRole('button', { name: "Login" }).click();
await expect(page).toHaveURL(/playground/);
// --- 準備工作結束 ---
// --- 尋找目標 [Cat 2] ---
await page.getByRole('button', { name: "Cat Zone" }).click();
await expect(page.getByRole('img', { name: "second cat" })).toBeVisible();
// --- 重複的清理工作 【登出、關閉瀏覽器】---
await page.getByRole('button', { name: "Logout" }).click();
await page.close();
// --- 清理工作結束 ---
});
// 第三個測試:找到第 1 隻狗狗
test('find the dog 1', async ({ page }) => {
// --- 重複的準備工作 【輸入帳號密碼、登入】---
await page.goto('/');
await page.locator('input[name="username"]').fill('ada');
await page.locator('input[name="password"]').fill('adaPassword');
await page.getByRole('button', { name: "Login" }).click();
await expect(page).toHaveURL(/playground/);
// --- 準備工作結束 ---
// --- 尋找目標 [Dog 1] ---
await page.getByRole('button', { name: "Dog Zone" }).click();
await expect(page.getByRole('img', { name: "first dog" })).toBeVisible();
// --- 重複的清理工作 【登出、關閉瀏覽器】---
await page.getByRole('button', { name: "Logout" }).click();
await page.close();
// --- 清理工作結束 ---
});
// 接下來還要找第 3 隻貓咪、第 2 隻狗狗、第 3 隻狗狗....
這樣撰寫測試腳本,似乎沒有太大問題,測試都能順利執行,但是,我們看不出測試與測試之間的關聯,缺乏結構的設計讓測試看起來非常雜亂無章,其次,我們一直在重複編寫同樣的測試步驟,每次測試開始前的準備工作與測試結束後的清理工作,總是在複製貼上,重複相同的內容。
最後,也是最重要的一點:難以維護,如果畫面上登入按鈕的文字從「Login」改成「Game Start」,那就必須修改每一個測試案例,若不小心漏掉一個或修改錯誤,那就會造成測試失敗,完全陷入了維護地獄!
讓我們來看看加上 Hooks 的測試:
// --- 重複的準備工作 【輸入帳號密碼、登入】---
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.locator('input[name="username"]').fill('ada');
await page.locator('input[name="password"]').fill('adaPassword');
await page.getByRole('button', { name: "Login" }).click();
await expect(page).toHaveURL(/playground/);
});
// --- 準備工作結束 ---
test.describe('Cat Quest', () => {
// 第一個測試:找到第 1 隻貓咪
test('find the cat 1', async ({ page }) => {
await page.getByRole('button', { name: "Cat Zone" }).click();
await expect(page.getByRole('img', { name: "first cat" })).toBeVisible();
});
// 第二個測試:找到第 2 隻貓咪
test('find the cat 2', async ({ page }) => {
await page.getByRole('button', { name: "Cat Zone" }).click();
await expect(page.getByRole('img', { name: "second cat" })).toBeVisible();
});
// 接下來還要找第 3 隻貓咪、第 4 隻貓咪....
});
test.describe('Dog Quest', () => {
// 第三個測試:找到第 1 隻狗狗
test('find the dog 1', async ({ page }) => {
await page.getByRole('button', { name: "Dog Zone" }).click();
await expect(page.getByRole('img', { name: "first dog" })).toBeVisible();
});
// 接下來還要找第 2 隻狗狗、第 3 隻狗狗....
});
// --- 重複的清理工作 【登出、關閉瀏覽器】---
test.afterEach(async ({ page }) => {
await page.getByRole('button', { name: "Logout" }).click();
await page.close();
});
// --- 清理工作結束 ---
如此一來,程式碼變得相當簡潔,可讀性提高許多,測試執行的順序會是:
beforeEach
→ describe Cat Quest
- find the cat 1
→ afterEach
beforeEach
→ describe Cat Quest
- find the cat 2
→ afterEach
beforeEach
→ describe Dog Quest
- test find the dog 1
→ afterEach
如果 beforeEach
或 afterEach
其中的步驟有任何的變動,我們也只需要修改一次即可。
💡Tips:
describe
進階設定:
- 測試依序執行,若有測試失敗則獨立重試
test.describe.configure({ mode: 'default' });
- 測試平行執行,測試速度變快
test.describe.configure({ mode: 'parallel' });
- 測試依序執行且相互依賴,其中一個測試失敗則中止後續測試,並從頭開始重試
test.describe.configure({ mode: 'serial' });
Hooks 的另一個妙用,就是疊加 Timeout,只需要在 beforeEach
Hooks 裡這樣寫:
test.beforeEach(async ({ page }, testInfo) => {
// 為使用這個 hooks 的每一個測試增加 30 秒 Timeout
testInfo.setTimeout(testInfo.timeout + 30 * 1000);
});
就能為與這個 Hooks 掛鉤的測試變更 Timeout 了。
在這裡,我們認識了 Hooks 使用方法,它讓程式碼變得更加結構化,擁有可複用與易於維護的便利性,接下來,我們要認識另一個參數化測試 (Parameterized Tests) 的應用方式,讓資料與測試邏輯分離,打造更乾淨的測試腳本。