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:

  1. Marnuje czas — Proces logowania trwający 2 sekundy na test oznacza, że pakiet 100 testów spędza ponad 3 minuty tylko na logowaniu.
  2. 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.
  3. 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 storageState bez 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 storageState do 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