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;

:::warning[Nigdy nie zapisuj danych logowania na stałe w kodzie] Zawsze czytaj dane logowania ze zmiennych środowiskowych, nigdy nie commituj ich do kontroli wersji. Używaj plików .env lokalnie i sekretów CI w pipeline’ach. :::

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

:::tip[Projektuj z myślą o równoległości] Projektując swój pakiet testów, zakładaj, że testy będą działać równolegle. Jeśli test modyfikuje stan konta, od którego zależą inne testy, albo izoluj go za pomocą .serial(), albo daj każdemu workerowi własne konto testowe. :::

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