Projektowanie pod kątem testowalności
Dowiedz się, jak współpracować z deweloperami, aby budować aplikacje łatwe do testowania — z szwami, test ID, feature flags i hakami API, które sprawiają, że automatyzacja jest niezawodna i łatwa w.
To jest część 4 serii Architektura testów. Jeśli przegapiłeś Część 3 — Obserwowalność dla testerów, zacznij tam.
Wprowadzenie — testowanie to problem projektowy
Najtrudniejszą częścią automatyzacji testów nie jest pisanie testów. To testowanie aplikacji, które nie zostały zaprojektowane do testowania.
Aplikacja bez test ID zmusza Cię do pisania kruchych selektorów CSS. Aplikacja bez szwów zmusza Cię do testowania dużych, splątanych przepływów w jednym teście. Aplikacja bez feature flags zmusza Cię do testowania w produkcji lub koordynowania złożonych wdrożeń do współdzielonych środowisk staging.
To nie są problemy testowe — to problemy projektowe. A rozwiązaniem nie są lepsze narzędzia testowe. Rozwiązaniem jest projektowanie pod kątem testowalności od początku.
Testowalność nie jest problemem QA. To problem deweloperski. Najlepsze zestawy testów są budowane na aplikacjach, które zostały zaprojektowane z myślą o testowaniu. To oznacza:
- Test ID dla niezawodnego wybierania elementów
- Szwy gdzie testy mogą wstrzykiwać zależności lub omijać wolne operacje
- Feature flags do kontrolowania, jaki kod działa w różnych środowiskach
- Haki API do seedowania danych i konfiguracji środowiska
Ten post pokaże Ci, jak współpracować z deweloperami, aby wbudować testowalność w aplikację — i jak to poprawia nie tylko testowanie, ale ogólną jakość i łatwość utrzymania codebase.
Zasada testowalności 1: Używaj test ID, nie selektorów CSS
Problem: Kruche selektory
Większość automatyzacji testów zaczyna się tak:
await page.click('.btn-primary.submit-order'); // selektor klasy CSS
Potem projektant zmienia styl przycisku:
- <button class="btn-primary submit-order">Złóż zamówienie</button>
+ <button class="btn-blue action-btn">Złóż zamówienie</button>
Test się psuje. Nie dlatego, że funkcjonalność się zmieniła — dlatego, że projekt wizualny się zmienił.
Rozwiązanie: Test ID
Dodaj atrybuty data-testid do elementów, z którymi testy muszą wchodzić w interakcję:
<button class="btn-blue action-btn" data-testid="place-order-button">
Złóż zamówienie
</button>
Teraz test używa test ID:
await page.click('[data-testid="place-order-button"]');
Projektant może zmieniać styl przycisku ile chce. Test się nie psuje.
:::tip[Test ID to kontrakt]
data-testid to kontrakt między aplikacją a zestawem testów. Mówi: „Ten element ma stabilny identyfikator do celów automatyzacji.” Deweloperzy powinni traktować test ID jako część powierzchni API — zmiana lub usunięcie ich to breaking change.
:::
Gdzie dodawać test ID
Nie każdy element potrzebuje test ID. Dodawaj je strategicznie:
- Akcje: Przyciski, linki, pola formularzy, z którymi użytkownicy wchodzą w interakcję
- Ważna treść: Nagłówki, komunikaty błędów, komunikaty potwierdzenia
- Dynamiczne listy: Elementy na liście, gdzie musisz wybrać konkretny element
Nie dodawaj test ID do:
- Elementów dekoracyjnych (ikony, separatory)
- Statycznego tekstu, który nigdy się nie zmienia
- Elementów, z którymi testy nie wchodzą w interakcję
Test ID w bibliotekach komponentów
Jeśli Twój zespół używa biblioteki komponentów (Material-UI, Ant Design, Chakra UI), dodawaj test ID jako propsy:
<Button data-testid="submit-form">Wyślij</Button>
<TextField data-testid="email-input" label="Email" />
<Alert data-testid="error-message" severity="error">Nieprawidłowy email</Alert>
Ustandaryzuj to w zespole. Uczyń to częścią API komponentu.
Zasada testowalności 2: Buduj szwy do izolacji testów
Szew to miejsce, w którym możesz zmienić zachowanie bez edytowania kodu. Szwy umożliwiają testowanie komponentu w izolacji bez uruchamiania całego systemu.
Przykład: Logika zależna od czasu
Kod nietestowalny:
function isPromotionActive(): boolean {
const now = new Date();
const promotionEnd = new Date('2026-12-31');
return now < promotionEnd;
}
Ta funkcja bezpośrednio wywołuje new Date(). W teście nie możesz kontrolować, co jest „teraz”. Test zawiedzie po 31 grudnia 2026.
Kod testowalny (ze szwem):
function isPromotionActive(now: Date = new Date()): boolean {
const promotionEnd = new Date('2026-12-31');
return now < promotionEnd;
}
Teraz test może wstrzyknąć konkretną datę:
test('promocja jest aktywna przed datą końcową', () => {
const testDate = new Date('2026-06-01');
expect(isPromotionActive(testDate)).toBe(true);
});
test('promocja jest nieaktywna po dacie końcowej', () => {
const testDate = new Date('2027-01-01');
expect(isPromotionActive(testDate)).toBe(false);
});
Przykład: Wywołania zewnętrznych API
Kod nietestowalny:
async function getUserProfile(userId: string) {
const response = await fetch(`https://api.example.com/users/${userId}`);
return response.json();
}
To bezpośrednio wywołuje zewnętrzne API. Testy są wolne, niestabilne i zależne od warunków sieciowych.
Kod testowalny (z wstrzykiwaniem zależności):
interface ApiClient {
getUser(userId: string): Promise<User>;
}
async function getUserProfile(userId: string, api: ApiClient) {
return api.getUser(userId);
}
Teraz test może wstrzyknąć mock API:
test('getUserProfile zwraca dane użytkownika', async () => {
const mockApi: ApiClient = {
getUser: async (userId) => ({ id: userId, name: 'Test User' })
};
const profile = await getUserProfile('123', mockApi);
expect(profile.name).toBe('Test User');
});
:::info[Szwy umożliwiają testy jednostkowe] Szwy są niezbędne do testów jednostkowych. Bez szwów nie możesz izolować testowanego kodu od jego zależności. Ze szwami możesz testować logikę niezależnie od baz danych, API i zewnętrznych serwisów. :::
Zasada testowalności 3: Używaj feature flags
Feature flags pozwalają wdrażać kod, który nie jest gotowy do produkcji, testować go w izolacji i stopniowo wprowadzać do użytkowników.
Dlaczego feature flags poprawiają testowalność
- Testuj w produkcji — wdróż niekompletne funkcje za flagą, włącz flagę tylko dla kont testowych
- Równoległy rozwój — wiele zespołów pracuje nad różnymi funkcjami bez blokowania się nawzajem
- Rollback bez wdrożenia — jeśli funkcja się psuje, wyłącz flagę zamiast wycofywać kod
Przykład: Stopniowe wprowadzanie
if (featureFlags.isEnabled('nowy-przeplyw-checkout', user)) {
return <NowyCheckout />;
} else {
return <StaryCheckout />;
}
QA może testować nowy checkout na stagingu z włączoną flagą. Użytkownicy produkcyjni nadal widzą stary checkout. Gdy nowy checkout jest zwalidowany, stopniowo włączaj flagę dla 10%, potem 50%, potem 100% użytkowników.
Testowanie kodu z feature flags
Bez feature flags: Musisz wdrożyć nową funkcję na staging, psując starą funkcję dla wszystkich innych testujących na stagingu.
Z feature flags: Włączasz flagę dla konkretnych kont testowych. Inni testerzy nie są dotknięci.
Przykład Playwright:
test('nowy przepływ checkout działa', async ({ page }) => {
// Włącz feature flag dla tego testu
await page.goto('/feature-flags?enable=nowy-przeplyw-checkout');
await page.goto('/koszyk');
await page.click('[data-testid="checkout-button"]');
// Nowe UI checkout jest teraz widoczne
await expect(page.locator('[data-testid="nowy-formularz-checkout"]')).toBeVisible();
});
:::warning[Czyść stare flagi] Feature flags gromadzą się z czasem i tworzą dług techniczny. Po pełnym wprowadzeniu i walidacji funkcji usuń flagę i usuń starą ścieżkę kodu. Flagi, które żyją wiecznie, to tylko warunki z dodatkowymi krokami. :::
Zasada testowalności 4: Zapewniaj haki API do seedowania danych
Testy potrzebują danych. Najgorszy sposób na dostarczenie danych to współdzielone konta testowe. Najlepszy sposób to API seedujące, które pozwalają testom tworzyć dane na żądanie.
Antywzorzec: Współdzielone dane testowe
test('użytkownik może zobaczyć profil', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'testuser@example.com'); // współdzielone konto
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login-button"]');
await page.goto('/profile');
await expect(page.locator('h1')).toHaveText('Test User');
});
Problem: Jeśli inny test (lub deweloper) modyfikuje testuser@example.com, ten test się psuje.
Wzorzec: Seeduj dane per test
Zbuduj API do seedowania danych testowych:
// Endpoint API seedowania (dostępny tylko w środowiskach staging/test)
POST /api/test/seed-user
{
"email": "user@example.com",
"name": "Test User",
"role": "customer"
}
Response:
{
"id": "user-abc123",
"email": "user@example.com",
"password": "generated-password-xyz"
}
Teraz test tworzy własnego użytkownika:
test('użytkownik może zobaczyć profil', async ({ page, request }) => {
// Seeduj unikalnego użytkownika dla tego testu
const response = await request.post('/api/test/seed-user', {
data: { email: `user-${Date.now()}@example.com`, name: 'Test User' }
});
const user = await response.json();
await page.goto('/login');
await page.fill('[data-testid="email"]', user.email);
await page.fill('[data-testid="password"]', user.password);
await page.click('[data-testid="login-button"]');
await page.goto('/profile');
await expect(page.locator('h1')).toHaveText('Test User');
});
Korzyści:
- Brak współdzielonego stanu
- Testy mogą działać równolegle
- Brak niestabilności z powodu zmian danych
Co uwzględnić w seed API
- Użytkownicy (z konkretnymi rolami, uprawnieniami, stanami subskrypcji)
- Produkty (z konkretnymi cenami, poziomami zapasów)
- Zamówienia (z konkretnymi statusami, pozycjami)
- Organizacje (z konkretnymi konfiguracjami)
:::tip[Seed API to także narzędzia deweloperskie] Seed API nie są tylko dla testów. Deweloperzy używają ich do szybkiego konfigurowania środowisk lokalnych. Menedżerowie produktu używają ich do demo funkcji z realistycznymi danymi. Zespoły wsparcia używają ich do reprodukowania problemów klientów. Zainwestuj w dobre narzędzia seedujące — opłacają się w całym zespole. :::
Zasada testowalności 5: Czyń przejścia stanów obserwowalnymi
Testy często zawodzą, ponieważ zakładają, że przejście stanu zostało zakończone, gdy tak nie jest. Aplikacja wciąż się ładuje. Żądanie API wciąż oczekuje. Animacja wciąż trwa.
Antywzorzec: Arbitralne oczekiwania
await page.click('[data-testid="submit-button"]');
await page.waitForTimeout(2000); // mam nadzieję, że skończy się w 2 sekundy
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
To jest niestabilne. Czasami 2 sekundy wystarczą. Czasami nie.
Wzorzec: Czekaj na obserwowalny stan
Ujawnij przejścia stanów w UI:
<div data-testid="form-container" data-state="idle">...</div>
<div data-testid="form-container" data-state="submitting">...</div>
<div data-testid="form-container" data-state="success">...</div>
<div data-testid="form-container" data-state="error">...</div>
Teraz test może czekać na zmianę stanu:
await page.click('[data-testid="submit-button"]');
await page.waitForSelector('[data-testid="form-container"][data-state="success"]');
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
To jest deterministyczne. Test czeka dokładnie tyle, ile potrzeba, nie więcej, nie mniej.
Współpraca z deweloperami
Testowalność to wspólna odpowiedzialność. QA nie może samodzielnie dodać test ID ani zbudować API seedujących — deweloperzy muszą priorytetyzować testowalność jako część pracy nad funkcją.
Jak promować testowalność
- Pokaż koszt nietestowalnego kodu — zmierz czas spędzony na debugowaniu niestabilnych testów spowodowanych brakującymi test ID lub współdzielonymi danymi
- Uczyń testowalność wymaganiem — dodaj „test ID dodane” i „seed API dostępne” do definicji ukończenia
- Paruj się z deweloperami — usiądź z deweloperem, gdy buduje funkcję, i wskaż luki w testowalności w czasie rzeczywistym
- Dokumentuj standardy — napisz przewodnik testowalności dla swojego zespołu (np. „Wszystkie elementy interaktywne muszą mieć
data-testid”)
Przykład: Checklista testowalności dla PR-ów
## Checklista testowalności
- [ ] Test ID dodane do wszystkich elementów interaktywnych
- [ ] Feature flag skonfigurowana (jeśli dotyczy)
- [ ] Endpoint API seedowania dostępny (jeśli nowy typ encji)
- [ ] Przejścia stanów obserwowalne (stany ładowania, sukcesu, błędu)
- [ ] Brak hardkodowanych timeoutów w testach
:::tip[Testowalność poprawia design] Gdy deweloperzy projektują pod kątem testowalności, piszą lepszy kod. Szwy wymuszają luźne powiązanie. Test ID wymuszają semantyczny HTML. Feature flags wymuszają modularną architekturę. Testowalność i dobry design idą w parze. :::
Przykład z rzeczywistości: Przeprojektowanie przepływu checkout
Przed: Nietestowalny checkout
- Brak test ID (testy używały
.btn-primary.checkout, które psuło się przy każdym redesignie) - Brak seed API (testy używały współdzielonych kont, które inne testy modyfikowały)
- Brak feature flag (nowy checkout blokował testowanie starego checkoutu na stagingu)
- Brak obserwowalnego stanu (testy używały
waitForTimeout(3000)i były niestabilne)
Rezultat: Testy psuły się co sprint. Koszt utrzymania przewyższał wartość. Zespół rozważał usunięcie zestawu testów.
Po: Testowalny checkout
- Dodano
data-testiddo każdego przycisku, inputu i wiadomości - Zbudowano
/api/test/seed-cartdo tworzenia zamówień na żądanie - Wdrożono nowy checkout za flagą
new-checkout-v2 - Dodano atrybuty
data-statedo formularza (idle,submitting,success,error)
Rezultat: Testy stały się niezawodne. Zestaw testów działał w CI bez niestabilności. Koszt utrzymania spadł o 80%. Zespół rozszerzył pokrycie testowe.
Podsumowanie
Testowalność nie jest problemem testowym — to problem projektowy. Najlepsze zestawy testów są budowane na aplikacjach, które zostały zaprojektowane do testowania.
Zasady:
- Używaj test ID — stabilne identyfikatory dla elementów interaktywnych
- Buduj szwy — wstrzykuj zależności, kontroluj czas, mockuj zewnętrzne serwisy
- Używaj feature flags — testuj w produkcji, równoległy rozwój, stopniowe wprowadzanie
- Zapewniaj seed API — twórz dane na żądanie, brak współdzielonych kont
- Czyń stan obserwowalnym — brak arbitralnych oczekiwań, deterministyczne przejścia
Gdy projektujesz pod kątem testowalności, nie tylko ułatwiasz testowanie — poprawiasz codebase. Luźne powiązanie. Modularna architektura. Jasne kontrakty. Testowalność to funkcja wymuszająca dobry design.
:::tip[Zacznij od małych kroków, buduj zaufanie] Jeśli Twój zespół jest nowy w projektowaniu pod kątem testowalności, zacznij od jednej funkcji. Dodaj test ID. Zbuduj seed API. Pokaż wpływ: mniej niestabilnych testów, szybszy feedback, mniej utrzymania. Następnie rozszerz. :::
Zadanie na ten tydzień: Wybierz jeden kruchy test, który często się psuje. Zidentyfikuj główną przyczynę: brakujące test ID? Współdzielone dane? Hardkodowane timeouty? Współpracuj z deweloperem, aby naprawić podstawowy problem projektowy. Zmierz wpływ: czy test staje się bardziej niezawodny?
To kończy serię Architektura testów. W przyszłym tygodniu rozpoczniemy nową serię: AI w QA — badając, jak narzędzia wspomagane AI pasują do projektowania testów, utrzymania i zarządzania ryzykiem. Do zobaczenia w Część 1 — Projektowanie testów z AI.