Auth, storage state i scenariusze wielu użytkowników
Autentykacja w testach jest zwodniczo trudna. Dowiedz się, jak użyć storageState w Playwright, aby zalogować się raz i używać sesji wielokrotnie, testować różne role użytkowników i uniknąć.
Ten wpis jest częścią serii Podstawy Playwright. Jeśli pominąłeś poprzedni post, przeczytaj najpierw Część 5 — Lokatory i asercje.
Wprowadzenie — autentykacja to nie krok testowy
Najczęstszym antywzorcem w automatyzacji testów end-to-end jest traktowanie autentykacji jako części samego testu. Każdy test loguje się na początku, wypełnia formularz logowania, wysyła go, czeka na nawigację, a dopiero potem zaczyna właściwy scenariusz testowy.
To podejście ma trzy problemy:
- Marnuje czas — Proces logowania trwający 2 sekundy na test oznacza, że pakiet 100 testów spędza ponad 3 minuty tylko na logowaniu.
- Zwiększa niestabilność — Logowanie obejmuje żądania sieciowe, przekierowania, zewnętrznych dostawców tożsamości i ograniczenia częstotliwości. Więcej kroków oznacza więcej okazji do przejściowych awarii.
- Mija się z celem — Autentykacja to infrastruktura. Twój test powinien skupić się na zachowaniu, które testujesz, a nie wielokrotnie udowadniać, że formularz logowania działa.
API storageState w Playwright rozwiązuje ten problem. Logujesz się raz, zapisujesz stan sesji (ciasteczka, localStorage, sessionStorage) i używasz go ponownie we wszystkich testach. Testy stają się szybsze, bardziej niezawodne i bardziej skoncentrowane.
Ten post pokaże ci, jak prawidłowo używać storageState, jak obsługiwać wiele ról użytkowników równolegle i jak unikać najczęstszych pułapek związanych z bezpieczeństwem i równoległością.
Problem: każdy test się loguje
Oto jak wygląda naiwna autentykacja:
test('użytkownik może wyświetlić dashboard', async ({ page }) => {
await page.goto('https://example.com/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'haslo123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
// Teraz właściwie zaczyna się test
await expect(page.locator('h1')).toHaveText('Dashboard');
});
test('użytkownik może edytować profil', async ({ page }) => {
// Powtórz cały proces logowania ponownie
await page.goto('https://example.com/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'haslo123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
// Teraz właściwie zaczyna się test
await page.goto('/settings/profile');
await expect(page.locator('input[name="displayName"]')).toBeVisible();
});
Jeśli masz 50 testów, właśnie wykonałeś proces logowania 50 razy. To 50 połączeń sieciowych, 50 zapytań do bazy danych, 50 generowań tokenów sesji. To wolne i marnotrawne.
Rozwiązanie: globalny setup i storage state
Playwright pozwala zalogować się raz w globalnym skrypcie setupu, zapisać stan uwierzytelnionej sesji do pliku i używać go ponownie we wszystkich testach.
Krok 1: Stwórz globalny skrypt setupu
Utwórz global-setup.ts w katalogu głównym projektu:
// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
// Wykonaj logowanie
await page.goto('https://example.com/login');
await page.fill('input[name="email"]', process.env.TEST_USER_EMAIL!);
await page.fill('input[name="password"]', process.env.TEST_USER_PASSWORD!);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
// Zapisz uwierzytelniony stan
await page.context().storageState({ path: 'auth/user.json' });
await browser.close();
}
export default globalSetup;
Krok 2: Skonfiguruj Playwright, aby używał setupu
Zaktualizuj playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalSetup: './global-setup.ts',
use: {
storageState: 'auth/user.json',
},
// ... reszta konfiguracji
});
Krok 3: Pisz testy bez logiki autentykacji
Teraz każdy test zaczyna się już uwierzytelniony:
test('użytkownik może wyświetlić dashboard', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.locator('h1')).toHaveText('Dashboard');
});
test('użytkownik może edytować profil', async ({ page }) => {
await page.goto('/settings/profile');
await expect(page.locator('input[name="displayName"]')).toBeVisible();
});
Krok logowania zniknął. Testy są szybsze, prostsze i bardziej skoncentrowane.
Testowanie wielu użytkowników: projekty dla różnych ról
Większość prawdziwych aplikacji ma wiele ról użytkowników: admin, zwykły użytkownik, gość. Musisz przetestować przepływy dla każdej roli, co oznacza, że potrzebujesz wielu uwierzytelnionych sesji.
Funkcja projektów w Playwright pozwala zdefiniować wiele konfiguracji, każda z własnym storageState.
Krok 1: Rozszerz globalny setup o obsługę wielu użytkowników
// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
import * as fs from 'fs';
async function globalSetup(config: FullConfig) {
const users = [
{
email: process.env.TEST_USER_EMAIL!,
password: process.env.TEST_USER_PASSWORD!,
storageStatePath: 'auth/user.json',
},
{
email: process.env.TEST_ADMIN_EMAIL!,
password: process.env.TEST_ADMIN_PASSWORD!,
storageStatePath: 'auth/admin.json',
},
];
// Upewnij się, że katalog auth istnieje
if (!fs.existsSync('auth')) {
fs.mkdirSync('auth');
}
const browser = await chromium.launch();
for (const user of users) {
const page = await browser.newPage();
await page.goto('https://example.com/login');
await page.fill('input[name="email"]', user.email);
await page.fill('input[name="password"]', user.password);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
await page.context().storageState({ path: user.storageStatePath });
await page.close();
}
await browser.close();
}
export default globalSetup;
Krok 2: Zdefiniuj projekty w playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
globalSetup: './global-setup.ts',
projects: [
{
name: 'user',
use: {
...devices['Desktop Chrome'],
storageState: 'auth/user.json',
},
},
{
name: 'admin',
use: {
...devices['Desktop Chrome'],
storageState: 'auth/admin.json',
},
},
],
});
Krok 3: Pisz testy specyficzne dla ról
// user.spec.ts
import { test, expect } from '@playwright/test';
test.use({ storageState: 'auth/user.json' });
test('zwykły użytkownik nie może uzyskać dostępu do panelu admina', async ({ page }) => {
await page.goto('/admin');
await expect(page.locator('text=Dostęp zabroniony')).toBeVisible();
});
// admin.spec.ts
test.use({ storageState: 'auth/admin.json' });
test('admin może uzyskać dostęp do panelu admina', async ({ page }) => {
await page.goto('/admin');
await expect(page.locator('h1')).toHaveText('Panel Admina');
});
Teraz możesz uruchamiać testy dla konkretnych ról:
npx playwright test --project=user
npx playwright test --project=admin
npx playwright test # uruchamia wszystkie projekty
Równoległa egzekucja i współdzielony stan
Gdy uruchamiasz testy równolegle (co Playwright robi domyślnie), każdy proces worker dostaje własny izolowany kontekst przeglądarki. Oznacza to:
- ✅ Każdy worker może używać tego samego pliku
storageStatebez konfliktów - ✅ Testy nie zakłócają nawzajem swoich sesji
- ⚠️ Ale jeśli twoje testy modyfikują konto (np. zmieniają email, usuwają profil), te zmiany nie są izolowane
Problem: mutacja współdzielonego konta
test('użytkownik może zmienić email', async ({ page }) => {
await page.goto('/settings/profile');
await page.fill('input[name="email"]', 'nowyemail@example.com');
await page.click('button[type="submit"]');
await expect(page.locator('text=Email zaktualizowany')).toBeVisible();
});
test('użytkownik może zobaczyć obecny email', async ({ page }) => {
await page.goto('/settings/profile');
// Ten test może teraz zobaczyć 'nowyemail@example.com' lub 'user@example.com'
// w zależności od kolejności wykonania
await expect(page.locator('input[name="email"]')).toHaveValue('user@example.com');
});
Jeśli te testy uruchomią się równolegle, drugi test może zawieść, ponieważ pierwszy zmienił email.
Rozwiązanie: użyj oddzielnych kont lub fixtures
Opcja 1: Konta testowe na workera
Utwórz wiele kont testowych (user1, user2, user3) i przypisz jedno na workera. workerIndex w Playwright może pomóc:
test.use({
storageState: async ({}, use, testInfo) => {
const workerIndex = testInfo.parallelIndex;
await use(`auth/user-${workerIndex}.json`);
},
});
Opcja 2: Resetuj stan w fixtures
Jeśli twoja aplikacja ma API do resetowania stanu konta testowego, opakuj je w fixture:
test.beforeEach(async ({ request }) => {
await request.post('/api/test/reset-account', {
data: { email: process.env.TEST_USER_EMAIL },
});
});
Opcja 3: Nie mutuj współdzielonego stanu
Projektuj testy tak, aby nie modyfikowały konta w sposób, który wpływa na inne testy. Na przykład, zamiast zmieniać email konta, testuj, że walidacja formularza działa bez faktycznego wysyłania.
Typowe pułapki
Pułapka 1: Commitowanie auth/*.json do Git
Plik JSON storageState zawiera ciasteczka sesji. Jeśli zacommitujesz go do Git, każdy z dostępem do repozytorium może podszyć się pod tego użytkownika.
Rozwiązanie: Dodaj auth/ do .gitignore:
# .gitignore
auth/
*.storage.json
Generuj pliki storageState w CI jako część setupu testów, nigdy ich nie commituj.
Pułapka 2: Wygasłe sesje
Jeśli twoje tokeny uwierzytelniania wygasają po 24 godzinach, a twoje CI działa codziennie, storageState z wczoraj jest teraz nieważny.
Rozwiązanie: Regeneruj storageState w globalSetup przy każdym uruchomieniu testów. To domyślne zachowanie, jeśli będziesz przestrzegać powyższego setupu.
Pułapka 3: Testowanie samego procesu logowania
Jeśli używasz storageState dla wszystkich testów, jak testujesz, że proces logowania działa?
Rozwiązanie: Napisz jeden dedykowany spec testowy bez storageState:
// auth.spec.ts
import { test, expect } from '@playwright/test';
test.use({ storageState: undefined }); // Rezygnuj z globalnego storageState
test('użytkownik może się zalogować', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', process.env.TEST_USER_EMAIL!);
await page.fill('input[name="password"]', process.env.TEST_USER_PASSWORD!);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
await expect(page.locator('h1')).toHaveText('Dashboard');
});
test('logowanie kończy się niepowodzeniem przy nieprawidłowych danych', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'zly@example.com');
await page.fill('input[name="password"]', 'zlehaslo');
await page.click('button[type="submit"]');
await expect(page.locator('text=Nieprawidłowe dane logowania')).toBeVisible();
});
Ten spec działa bez storageState, więc testuje proces logowania od zera.
Kiedy nie używać storage state
storageState nie zawsze jest właściwym wyborem. Używaj go, gdy:
- ✅ Większość testów wymaga autentykacji
- ✅ Autentykacja jest wolna (SAML, OAuth, MFA)
- ✅ Testy są niezależne i nie modyfikują uwierzytelnionego użytkownika
Nie używaj storageState, gdy:
- ❌ Testujesz sam proces autentykacji
- ❌ Testujesz rejestrację konta
- ❌ Testujesz resetowanie hasła lub weryfikację emaila
- ❌ Każdy test wymaga unikalnego użytkownika (np. testowanie interakcji użytkownik-użytkownik)
W tych przypadkach uwierzytelniaj inline jako część testu.
Podsumowanie
Autentykacja to infrastruktura, nie logika testowa. storageState w Playwright pozwala zalogować się raz, zapisać sesję i używać jej w setkach testów. To sprawia, że testy są szybsze, bardziej niezawodne i łatwiejsze w utrzymaniu.
Kluczowe wnioski:
- Użyj
globalSetup, aby uwierzytelnić się raz i zapisaćstorageState - Użyj projektów, aby obsługiwać wiele ról użytkowników
- Nigdy nie commituj plików
storageStatedo kontroli kodu źródłowego - Uważaj na równoległą egzekucję, jeśli testy mutują współdzielony stan konta
- Pisz dedykowane testy dla samego procesu autentykacji bez
storageState
Zadanie na ten tydzień: Zrefaktoryzuj swój pakiet testów, aby używał storageState. Zmierz, ile czasu oszczędza przy pełnym uruchomieniu testów. Jeśli masz wiele ról użytkowników, dodaj projekty dla każdej roli i sprawdź, czy testy specyficzne dla ról działają poprawnie.
Następny w tej serii: Część 7 — Testy wizualne i dostępności w Playwright