Warstwowa architektura testów dla rosnących produktów

Dowiedz się, jak organizować testy na poziomie jednostkowym, API i UI, aby uniknąć duplikacji pokrycia, wyjaśnić odpowiedzialność i skalować zestaw testów w miarę rozwoju produktu.

To jest pierwsza część serii Architektura testów. Przyjrzymy się, jak budować zestawy testów, które skalują się wraz z produktem, unikają redundancji i zapewniają szybki, niezawodny feedback.


Wprowadzenie — dlaczego warstwy mają znaczenie

Gdy produkt jest mały, architektura testów nie ma większego znaczenia. Piszesz testy tam, gdzie wydaje się to naturalne, uruchamiasz je, gdy pamiętasz, i wypuszczasz kod, gdy wydaje się, że wszystko działa. Pętla zwrotna jest szybka, bo codebase jest mały.

Potem produkt rośnie. Zespół rośnie. Więcej funkcji. Więcej integracji. Więcej przypadków brzegowych. Zestaw testów, który wydawał się lekki i użyteczny, teraz trwa 40 minut, zawodzi nieprzewidywalnie i nikt nie jest pewien, co każdy test faktycznie weryfikuje.

To moment, w którym warstwowa architektura testów staje się krytyczna.

Warstwowa architektura testów organizuje testy według granicy systemu, na której działają: jednostkowe, API i UI. Każda warstwa ma wyraźny cel, prędkość i sygnał awarii. Testy są celowo umieszczane w warstwie, w której zapewniają maksymalną wartość przy minimalnym koszcie.

Bez tej struktury zespoły dryfują w kierunku jednego z dwóch trybów porażki:

  1. Nadmierne poleganie na testach UI — wolne, kruche, drogie w utrzymaniu
  2. Nadmierne poleganie na testach jednostkowych — szybkie, ale brakuje im luk integracyjnych

Obie skrajności tworzą martwe punkty. Warstwowa architektura rozwiązuje to, rozkładając odpowiedzialność między warstwy na podstawie tego, co każda warstwa robi najlepiej.


Trzy warstwy

Warstwa jednostkowa — logika i zachowanie w izolacji

Cel: Testować pojedyncze funkcje, klasy lub moduły w izolacji. Walidować logikę biznesową, przypadki brzegowe i obsługę błędów bez angażowania zewnętrznych zależności.

Charakterystyka:

  • Szybkie — milisekundy na test
  • Skupione — testują jedno zachowanie na raz
  • Izolowane — bez bazy danych, bez sieci, bez systemu plików

Co tutaj testować:

  • Logika biznesowa: obliczenia, reguły walidacji, przejścia stanów
  • Przypadki brzegowe: puste dane wejściowe, warunki graniczne, nieprawidłowe dane
  • Obsługa błędów: wyjątki, ponowne próby, logika zapasowa

Przykład (TypeScript):

// Test jednostkowy dla kalkulatora cen
describe('calculateDiscountedPrice', () => {
  it('stosuje 10% rabatu, gdy suma przekracza 100 zł', () => {
    expect(calculateDiscountedPrice(120)).toBe(108);
  });

  it('rzuca błąd dla ujemnych kwot', () => {
    expect(() => calculateDiscountedPrice(-10)).toThrow('Nieprawidłowa kwota');
  });
});

:::tip[Własność testów jednostkowych] Testy jednostkowe są własnością deweloperów. Uruchamiają się przed commitem, w IDE, jako część dev workflow. Jeśli test jednostkowy zawiedzie, deweloper naprawia go przed pushowaniem kodu. :::

Warstwa API — integracja bez UI

Cel: Testować interakcję między komponentami przez warstwę API. Walidować obsługę żądań, operacje bazodanowe, uwierzytelnianie i przepływy biznesowe bez angażowania UI.

Charakterystyka:

  • Umiarkowana prędkość — sekundy na test
  • Rzeczywiste integracje — faktyczna baza danych, prawdziwe auth, prawdziwe serwisy (lub realistyczne mocki)
  • Niezależne od UI — testy przetrwają redesign UI

Co tutaj testować:

  • Walidacja żądań i odpowiedzi błędów
  • Transakcje bazodanowe i integralność danych
  • Reguły uwierzytelniania i autoryzacji
  • Wieloetapowe przepływy (np. stwórz konto → zweryfikuj email → zaloguj)

Przykład (Playwright API testing):

test('POST /orders waliduje wymagane pola', async ({ request }) => {
  const response = await request.post('/api/orders', {
    data: { productId: 123 } // brak quantity
  });
  
  expect(response.status()).toBe(400);
  const body = await response.json();
  expect(body.errors).toContain('quantity jest wymagane');
});

:::info[Testy API to sweet spot] Testy API zapewniają silne pokrycie integracyjne za ułamek kosztu testów UI. Wychwytują problemy konfiguracyjne, problemy z bazą danych i błędy integracji, które testy jednostkowe pomijają — bez kruchości automatyzacji UI. :::

Warstwa UI — ścieżki użytkownika i kontrakty wizualne

Cel: Testować kompletne doświadczenie użytkownika przez przeglądarkę. Walidować, że krytyczne ścieżki użytkownika działają end-to-end i że UI zachowuje się poprawnie w rzeczywistych scenariuszach.

Charakterystyka:

  • Wolne — sekundy do minut na test
  • Kruche — wrażliwe na zmiany UI, problemy z czasem, różnice między przeglądarkami
  • Kompleksowe — testują cały stack

Co tutaj testować:

  • Krytyczne ścieżki użytkownika (rejestracja, checkout, płatność)
  • Zachowania specyficzne dla UI (modale, drag-and-drop, responsive layout)
  • Regresje wizualne (porównanie screenshotów)
  • Dostępność (nawigacja klawiaturą, kompatybilność ze screen readerami)

Czego NIE testować tutaj:

  • Przypadków brzegowych już pokrytych przez testy jednostkowe
  • Logiki walidacji już pokrytej przez testy API
  • Każdego możliwego stanu UI (kombinatoryczna eksplozja)

Przykład (Playwright UI test):

test('użytkownik może ukończyć proces checkout', async ({ page }) => {
  await page.goto('/koszyk');
  await page.click('text=Przejdź do kasy');
  await page.fill('#email', 'klient@example.com');
  await page.fill('#cardNumber', '4242424242424242');
  await page.click('text=Złóż zamówienie');
  
  await expect(page.locator('text=Zamówienie potwierdzone')).toBeVisible();
});

:::warning[Testy UI są drogie] Testy UI są najdroższe do napisania, uruchomienia i utrzymania. Pisz testy UI tylko dla przepływów, które naprawdę wymagają walidacji end-to-end. Jeśli to samo zachowanie można przetestować na poziomie API lub jednostkowym, testuj je tam. :::


Unikanie duplikacji pokrycia

Najczęstszym błędem w warstwowej architekturze testów jest testowanie tego samego na wielu warstwach.

Przykład: walidacja hasła

Zły podejście — testowanie walidacji na wszystkich trzech warstwach:

  • Test jednostkowy: waliduje długość hasła, wymagania dotyczące znaków
  • Test API: wysyła nieprawidłowe hasła i sprawdza odpowiedzi 400
  • Test UI: wpisuje nieprawidłowe hasła i sprawdza komunikaty błędów

Prawidłowe podejście — testuj raz na odpowiedniej warstwie:

  • Test jednostkowy: kompleksowa logika walidacji (długość, znaki, złożoność)
  • Test API: jeden smoke test potwierdzający, że walidacja jest wywoływana
  • Test UI: jeden smoke test potwierdzający, że komunikat błędu wyświetla się poprawnie

Zasada: testuj na najniższej warstwie, która daje pewność

Jeśli test jednostkowy dowodzi, że logika walidacji działa, nie potrzebujesz 50 testów API próbujących każdego przypadku brzegowego. Test API tylko potwierdza, że jednostka jest prawidłowo podłączona. Test UI tylko potwierdza, że komunikat błędu się renderuje.

Korzyści:

  • Szybszy zestaw testów (mniej wolnych testów)
  • Niższy koszt utrzymania (jedno miejsce do aktualizacji, gdy logika się zmienia)
  • Wyraźniejsze sygnały awarii (lokalizacja awarii = warstwa rzeczywistego problemu)

Własność na warstwę

Każda warstwa powinna mieć jasną własność — kogoś odpowiedzialnego za utrzymanie tej warstwy, naprawę awarii i zapewnienie, że testy pozostają wartościowe.

WarstwaGłówny właścicielUruchamiane kiedyCel
JednostkowaDeweloperzyPre-commit, przy zapisieSzybki feedback na zmiany logiki
APIInżynierowie QA lub deweloperzyPipeline PRPewność integracji
UIInżynierowie QANocne lub pre-releaseWalidacja ścieżek end-to-end

:::tip[Współdzielona własność dla testów API] Testy API siedzą na granicy między dev i QA. Niektóre zespoły mają deweloperów piszących testy API dla nowych endpointów, a następnie QA rozszerza je o dodatkowe scenariusze. To działa dobrze, ponieważ testy API to kod — korzystają z code review i narzędzi deweloperskich. :::


Porty między warstwami — zasada zależności

Dobrze zaprojektowana architektura testów szanuje granice warstw. Testy na wyższej warstwie nie powinny omijać ani bezpośrednio uzyskiwać dostępu do logiki z niższej warstwy.

Przykład:

  • Test UI nie powinien mockować odpowiedzi API na poziomie sieciowym (użyj prawdziwego API)
  • Test API nie powinien bezpośrednio wywoływać zapytań bazodanowych (użyj endpointu API)

Dlaczego? Ponieważ omijanie warstw niweczy cel testów integracyjnych. Jeśli Twój test API mockuje bazę danych, nie testujesz integracji między API a bazą danych — testujesz fantazyjną wersję systemu, która nie istnieje w produkcji.

Wyjątek: Mockowanie zewnętrznych serwisów (bramki płatności, API innych firm) jest akceptowalne i często konieczne, aby testy były deterministyczne i szybkie.


Praktyczny przykład — checkout e-commerce

Zastosujmy warstwową architekturę do funkcji checkout e-commerce.

Warstwa jednostkowa

  • Obliczanie ceny z podatkiem i rabatami
  • Logika sprawdzania dostępności zapasów
  • Reguły walidacji płatności

Warstwa API

  • POST /cart/add — dodaje przedmiot, aktualizuje zapasy
  • POST /checkout — waliduje koszyk, przetwarza płatność, tworzy zamówienie
  • GET /orders/:id — pobiera potwierdzenie zamówienia

Warstwa UI

  • Jeden test ścieżki krytycznej: Dodaj przedmiot do koszyka → przejdź do kasy → wprowadź dane płatności → potwierdź zamówienie → zobacz stronę potwierdzenia

Zauważ: warstwa UI ma jeden test. Logika walidacji, sprawdzenia zapasów i reguły płatności są już udowodnione na niższych warstwach. Test UI po prostu potwierdza, że happy path działa end-to-end w przeglądarce.


Podsumowanie

Warstwowa architektura testów nie polega na pisaniu większej liczby testów — chodzi o pisanie właściwych testów we właściwych miejscach. Każda warstwa ma cel:

  • Testy jednostkowe dowodzą poprawności logiki
  • Testy API dowodzą niezawodności integracji
  • Testy UI dowodzą, że ścieżki użytkownika działają end-to-end

Unikaj duplikacji. Testuj na najniższej warstwie, która daje Ci pewność. Przypisz jasną własność. Szanuj granice warstw.

Ta struktura skaluje się. W miarę rozwoju produktu dodajesz testy do odpowiedniej warstwy bez rozdymania wolnej, kruchej górnej warstwy.

:::tip[Zacznij od małych kroków] Jeśli wprowadzasz warstwową architekturę do istniejącego projektu, zacznij od jednej funkcji. Zrefaktoryzuj jej testy na warstwy jednostkową, API i UI. Zmierz poprawę prędkości i redukcję utrzymania. Następnie rozszerz. :::

Zadanie na ten tydzień: Wybierz jedną funkcję, którą przetestowałeś. Nakreśl, które testy mogłyby przejść na niższą warstwę. Zidentyfikuj duplikację pokrycia. Zrefaktoryzuj jeden zestaw testów, aby stosował model warstwowy, i zmierz zaoszczędzony czas.


Dalej w serii: Część 2 — Środowiska testowe bez wspólnego chaosu, gdzie zajmiemy się izolacją środowisk testowych, seedowaniem danych i jak uniknąć problemu „ktoś zepsuł staging”.