iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
自我挑戰組

Playwright 練功房:從零開始的 30 天 E2E 測試教學筆記系列 第 26 篇

Day 26 - Global Setup + storageState:讓登入只要做一次的方法

  • 分享至 

  • xImage
  •  

前面我們用 Fixture 處理登入,在每支測試開始前,fixture 會先幫它登入好,測試裡不用再重複寫登入步驟。不過在這個做法下,每一支測試還是各自登入了一次。

今天要介紹另一種方式可以讓所有測試只登入一次,之後每支測試一打開就是已登入的狀態,要做到這件事,需要用到 Playwright 的 storageState,再搭配 setup,在所有測試開始前先完成登入。

storageState 是什麼?

在 Playwright 的架構下,登入後的狀態(cookie、localStorage)會儲存在 context 裡,而 page 只是 context 底下的分頁。

storageState 做的事情就是:

  1. 存:把某個 context 目前的 cookie 與 localStorage 匯出成一個 JSON 檔
  2. 載入:建立新的 context 時指定這個檔案,新的 context 一開始就帶著這些狀態

載入時,每個 context 拿到的都是檔案內容的複本。測試中對 localStorage 做的任何修改只留在自己的 context 裡,不會寫回檔案,也不會影響其他測試。

實際存了什麼

以 react-admin demo 為例,登入後存出來的檔案大概長這樣:

{
  "cookies": [],
  "origins": [
    {
      "origin": "http://localhost:8000",
      "localStorage": [
        { "name": "username", "value": "demo" }
      ]
    }
  ]
}

cookies 是空的,因為這個 demo 的登入是前端模擬的,但在有真實後端的專案,登入狀態通常是存在 cookie 裡的 session 或 token,所以 cookies 不會是空的。

打開 authProvider.ts 可以看到,登入後只做了一件事:

localStorage.setItem('username', username);

之後判斷是否已登入,會檢查 localStorage 裡有沒有 username。

setup 是什麼?

了解了 storageState,接下來的問題是:這份檔案要由誰、在什麼時候產生?

答案是在所有測試開始之前,先跑一次登入,產生 storageState 的檔案,這個測試前的準備步驟就是 setup。

要讓 setup 在所有測試之前執行,要透過 project 來設定。

什麼是 project?

在 playwright.config.ts 中,有一段 projects 設定:

projects: [
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
  { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
],

一個 project 就是「一組測試 + 一組執行設定」。預設的三個 project 設定的是不同瀏覽器,所以同一支測試會在 Chromium、Firefox、WebKit 各跑一次。之前執行指令時加的 --project=chromium,就是指定只跑 chromium 這個 project。

project 除了可以設定瀏覽器,還能設定:

  • 要跑哪些測試檔(testMatch)
  • 要等哪個 project 跑完才開始(dependencies)
  • 每支測試開 context 時要帶什麼設定(use,例如今天的 storageState)

用 project 設定 setup

詳細做法如下:

  1. 新增一個名為 setup 的 project,它只負責執行登入的那支檔案
  2. 在原本的 chromium、firefox、webkit 三個 project 加上 dependencies: ['setup'],意思是「這個 project 要等 setup 跑完才開始」
  3. 三個 project 同時設定 storageState,讓裡面的每支測試都載入 setup 產生的檔案

執行順序會變成這樣:

setup(登入一次,存成 user.json)
   │
   ├──> chromium(每支測試載入 user.json)
   ├──> firefox (每支測試載入 user.json)
   └──> webkit  (每支測試載入 user.json)

setup 跑完、檔案產生之後,三個瀏覽器的測試才平行開始,每支測試開新的 context 時載入同一份檔案。

補充:Playwright 另外還有一個 globalSetup 設定,也能在測試前執行一段程式。不過它是在測試框架之外執行,報告裡看不到它的執行過程,也不能使用 fixture,官方文件目前較推薦的是今天介紹的 setup project 寫法。

怎麼做

步驟一:寫 setup 檔

在 tests/ 底下新增 auth.setup.ts,登入的部分直接沿用之前寫好的 LoginPage:

import { test as setup, expect } from '@playwright/test';
import path from 'path';
import { LoginPage } from '../pages/LoginPage';

const authFile = path.join(__dirname, '../playwright/.auth/user.json');

setup('登入並儲存 storageState', async ({ page }) => {
    console.log('>>> setup:執行登入流程');
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login('demo', 'demo');

    // 確認登入完成才存檔
    await expect(
        page.getByRole('heading', { name: 'Welcome to the react-admin e-commerce demo' })
    ).toBeVisible();

    await page.context().storageState({ path: authFile });
});

你可能注意到第一行寫的是 test as setup,這裡的 setup 其實就是平常寫測試用的那個 test,只是改名叫 setup,讓人一眼看出這支檔案是用來做準備工作的。寫成 test('登入並儲存 storageState', ...) 效果也完全一樣。

換句話說,auth.setup.ts 本身就是一支普通的測試檔,所以跟一般測試一樣能使用 page 這些 fixture,執行過程也會出現在報告裡。它之所以會在其他測試之前執行,靠的不是檔案寫法,而是靠 config 裡的設定。

步驟二:修改 config

在 playwright.config.ts 的 projects 裡加上 setup project,並在原本三個瀏覽器的 project 加上 dependencies 與 storageState:

import path from 'path';
const authFile = path.join(__dirname, 'playwright/.auth/user.json');

export default defineConfig({
  // ...其他設定不變
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'], storageState: authFile },
      dependencies: ['setup'],
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'], storageState: authFile },
      dependencies: ['setup'],
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'], storageState: authFile },
      dependencies: ['setup'],
    },
  ],
});

上面設定檔的意義如下:

  • testMatch:setup project 只執行檔名是 .setup.ts 結尾的檔案
  • dependencies: ['setup']:這個 project 要等 setup 跑完才開始
  • storageState:這個 project 的每個 context 都載入這份檔案

步驟三:測試直接用內建的 page

現在測試裡不需要任何登入 fixture,直接使用內建的 page,打開就是已登入的狀態:

import { test, expect } from '@playwright/test';
import { ProductsPage } from '../pages/ProductsPage';

test('直接進商品列表,不會被導到登入頁', async ({ page }) => {
    await page.goto('/#/products');
    await expect(page.getByRole('button', { name: 'Sign in' })).not.toBeVisible();
    await expect(page).toHaveURL(/#\/products/);
});

test('巡檢 1:搜尋 Black', async ({ page }) => {
    await page.goto('/');
    const productsPage = new ProductsPage(page);
    await productsPage.goto();
    await productsPage.search('Black');
    await expect(productsPage.resultCount).toBeVisible();
});

test('巡檢 2:商品列表預設顯示 Aerial Coast', async ({ page }) => {
    await page.goto('/');
    const productsPage = new ProductsPage(page);
    await productsPage.goto();
    await expect(productsPage.productCard('Aerial Coast')).toBeVisible();
});

巡檢 1、2 是之前緩衝日實驗用過的測試,當時因為共用 context,巡檢 1 的搜尋條件會汙染巡檢 2。今天拿來確認改用 storageState 後,測試之間是否互相獨立。

驗證:登入真的只做一次嗎?

用一個 worker 執行,讓三支測試依序在同一個 worker 裡跑,重現當時共用 context 被汙染的條件:

npx playwright test day26-storage-state --project=chromium --workers=1
Running 4 tests using 1 worker
[setup] › tests/auth.setup.ts:7:6 › 登入並儲存 storageState
>>> setup:執行登入流程
  4 passed (5.9s)

可以看到:

  1. 登入只跑了一次:>>> setup:執行登入流程 只印一次,後面三支測試都沒有再登入
  2. 巡檢 2 通過了:巡檢 1 搜尋 Black 時寫進 localStorage 的篩選條件,只留在巡檢 1 自己的 context 裡;巡檢 2 開新的 context 時,載入的是 user.json 的乾淨複本,所以列表不會被篩選

再做一個小實驗:把巡檢 2 的 'Aerial Coast' 暫時改成 'NotExist' 讓它失敗,打開 HTML 報告,會看到巡檢 2 有一支屬於自己的影片,而且影片從首頁開始,沒有登入過程。整個過程我們沒有寫任何處理影片的程式碼。

不過要注意,storageState 隔離的是瀏覽器裡的狀態。如果測試是在後端資料庫新增了一筆資料,每個 context 重新載入時都還是會看到它,這部分需要在測試結束後清理,後面介紹 Global Teardown 時會再處理。

兩個要注意的地方

1. 存檔前要確認登入已經完成: 如果按下登入後馬上存檔,可能在登入狀態還沒寫進去之前就存了,得到一份「未登入」的檔案。所以 setup 裡先用 expect 確認進到後台,再呼叫 storageState()。

2. setup 只做登入,不要順便做別的事: storageState 會把當下 localStorage 的內容全部存下來。react-admin 會把列表的篩選、排序等狀態也存在 localStorage,如果 setup 登入後順手逛了列表頁、做了搜尋,這些狀態也會被存進檔案,這樣之後每一支測試打開也會帶著這些狀態,造成測試不獨立。

小結

  1. storageState:用 context.storageState({ path }) 存,在 config 的 use.storageState 載入。這個 demo 只存了 localStorage 裡的 username,真實專案通常是 cookie。
  2. setup project:project 是「一組測試 + 一組執行設定」;把登入寫成獨立的 setup project,讓其他的 project 在這個 setup project 執行完後才執行。
  3. 登入只做一次:每支測試仍然拿到全新的 context,載入的是檔案的乾淨複本,測試之間互不影響,失敗時也會自動附上自己的影片。

登入只做一次之後,接下來要面對的是:如果不同測試需要不同身分的使用者,例如管理員和一般使用者,storageState 要怎麼管理?下一篇我們來看多角色與多帳號的做法。


上一篇
Day 25 - 網路攔截與 Mock API:讓測試決定 API 回什麼
系列文
Playwright 練功房:從零開始的 30 天 E2E 測試教學筆記 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言