Ryzyka testów generowanych przez AI — checklista
Zbuduj systematyczną checklistę przeglądu dla testów generowanych przez AI, aby wychwycić halucynowane pokrycie, brakujące asercje, kruche selektory i nietestowane przypadki brzegowe zanim trafią do.
To jest część 3 (finałowa) serii AI w QA. Jeśli przegapiłeś Część 2 — AI w utrzymaniu testów, zacznij tam.
Wprowadzenie — ukryte ryzyka testów generowanych przez AI
Testy generowane przez AI wyglądają profesjonalnie. Mają opisowe nazwy, jasne asercje i dobrze ustrukturyzowany kod. Przechodzą w CI. Zwiększają metryki pokrycia.
Ale mają niebezpieczną właściwość: mogą przechodzić bez testowania czegokolwiek znaczącego.
To jest ryzyko halucynowanego pokrycia — testy, które istnieją, które działają, które raportują sukces, ale które faktycznie nie walidują zachowania, które twierdzą, że testują. AI nie rozumie Twojej aplikacji. Zgaduje, co testy powinny robić na podstawie wzorców, które widziało w danych treningowych. Czasami zgaduje źle.
Test, który nie testuje, jest gorszy niż brak testu. Tworzy fałszywą pewność. Konsumuje wysiłek utrzymania. Spowalnia CI. A gdy pojawia się prawdziwy bug, test i tak przechodzi.
Ten post pomoże Ci zbudować checklistę przeglądu dla testów generowanych przez AI — systematyczny proces wychwytywania halucynowanego pokrycia, brakujących asercji, kruchych selektorów i nietestowanych przypadków brzegowych zanim trafią do Twojej głównej gałęzi.
Ryzyko 1: Halucynowane pokrycie
Problem: AI generuje test, który wygląda dobrze, ale faktycznie nie weryfikuje zachowania, które twierdzi, że testuje.
Przykład: Fałszywa asercja
Test generowany przez AI:
test('użytkownik może zresetować hasło', async ({ page }) => {
await page.goto('/forgot-password');
await page.fill('[data-testid="email"]', 'user@example.com');
await page.click('[data-testid="submit-button"]');
// AI zakłada, że komunikat sukcesu się pojawia
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
});
Bug: Element komunikatu sukcesu istnieje w DOM cały czas (ukryty CSS display: none). Test przechodzi, nawet jeśli email resetujący hasło nigdy nie został wysłany.
Jak to wychwycić:
- Ręcznie wykonaj scenariusz testowy w aplikacji
- Zweryfikuj, że asercja jest faktycznie wyzwalana — usuń asercję i potwierdź, że test zawodzi
- Sprawdź, czy element jest warunkowo renderowany, a nie tylko warunkowo pokazywany
Naprawa:
test('użytkownik może zresetować hasło', async ({ page, request }) => {
await page.goto('/forgot-password');
await page.fill('[data-testid="email"]', 'user@example.com');
await page.click('[data-testid="submit-button"]');
// Czekaj, aż komunikat sukcesu zostanie wyrenderowany (nie tylko odkryty)
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
// Dodatkowa weryfikacja: sprawdź, czy email resetujący został wysłany
const emails = await request.get('/api/test/emails'); // endpoint tylko testowy
const resetEmail = emails.find(e => e.to === 'user@example.com' && e.subject.includes('Reset'));
expect(resetEmail).toBeDefined();
});
:::warning[Przechodzące testy ≠ Działające testy] Przechodzący test nie jest dowodem, że test działa. Zawsze weryfikuj, że test zawodzi, gdy powinien przed zaufaniem mu. :::
Ryzyko 2: Brakujące testy negatywne
AI ma tendencję do happy path testów. Generuje testy walidujące scenariusze sukcesu, ale często pomija scenariusze awarii.
Przykład: Brakująca obsługa błędów
Zestaw testów generowany przez AI:
test('stwórz zamówienie z prawidłowymi danymi', async ({ request }) => {
const response = await request.post('/api/orders', {
data: { productId: '123', quantity: 2 }
});
expect(response.status()).toBe(201);
});
Co brakuje:
- Co się dzieje, jeśli
productIdjest nieprawidłowy? - Co się dzieje, jeśli
quantityto 0 lub liczba ujemna? - Co się dzieje, jeśli produkt jest niedostępny?
- Co się dzieje, jeśli użytkownik nie jest uwierzytelniony?
Pytanie przeglądowe: Dla każdego testu happy path generowanego przez AI zapytaj: „Co powinno zawieść tutaj i jak?”
Naprawa: Dodaj testy negatywne:
test('tworzenie zamówienia zawodzi z nieprawidłowym ID produktu', async ({ request }) => {
const response = await request.post('/api/orders', {
data: { productId: 'invalid', quantity: 2 }
});
expect(response.status()).toBe(404);
const body = await response.json();
expect(body.error).toContain('Produkt nie znaleziony');
});
test('tworzenie zamówienia zawodzi z ujemną ilością', async ({ request }) => {
const response = await request.post('/api/orders', {
data: { productId: '123', quantity: -1 }
});
expect(response.status()).toBe(400);
});
Ryzyko 3: Kruche selektory
AI często generuje selektory, które są kruche — działają dziś, ale zepsują się przy drobnych zmianach UI.
Przykład: Selektor klasy CSS
Test generowany przez AI:
await page.click('button.btn-primary.submit-order');
Problem: Ten selektor zależy od klas CSS, które mogą się zmienić, gdy projektant zaktualizuje styl przycisku.
Pytanie przeglądowe: Czy ten selektor używa stabilnych identyfikatorów (data-testid, aria-label, semantyczny HTML)?
Naprawa:
await page.click('[data-testid="submit-order-button"]');
Ryzyko 4: Hardkodowane dane testowe
AI często generuje testy z hardkodowanymi danymi, które zależą od istniejącego stanu w bazie danych.
Przykład: Założenie, że użytkownik istnieje
Test generowany przez AI:
test('użytkownik może zobaczyć profil', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'testuser@example.com');
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: Ten test zakłada, że testuser@example.com istnieje w bazie danych. Jeśli inny test modyfikuje lub usuwa tego użytkownika, ten test zawodzi.
Pytanie przeglądowe: Czy ten test tworzy własne dane testowe, czy zależy od istniejących współdzielonych danych?
Naprawa: Seeduj dane per test:
test('użytkownik może zobaczyć profil', async ({ page, request }) => {
const user = await request.post('/api/test/seed-user', {
data: { email: `user-${Date.now()}@example.com`, name: 'Test User' }
});
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');
});
Ryzyko 5: Arbitralne oczekiwania
AI często generuje testy z hardkodowanymi timeoutami zamiast czekać na deterministyczne zmiany stanu.
Przykład: waitForTimeout
Test generowany przez AI:
await page.click('[data-testid="submit-button"]');
await page.waitForTimeout(3000); // czekaj 3 sekundy
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
Problem: Ten test jest niestabilny. Czasami 3 sekundy wystarczą. Czasami nie. Czasami to za dużo (test jest wolniejszy niż potrzeba).
Pytanie przeglądowe: Czy ten test używa arbitralnych oczekiwań, czy czeka na obserwowalne zmiany stanu?
Naprawa:
await page.click('[data-testid="submit-button"]');
await expect(page.locator('[data-testid="success-message"]')).toBeVisible({ timeout: 10000 });
Lub lepiej, czekaj na konkretny atrybut 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();
Ryzyko 6: Niekompletne asercje
Testy generowane przez AI czasami mają słabe asercje, które nie w pełni walidują zachowania.
Przykład: Sprawdzanie tylko kodu statusu
Test generowany przez AI:
test('stwórz użytkownika', async ({ request }) => {
const response = await request.post('/api/users', {
data: { email: 'user@example.com', name: 'Test User' }
});
expect(response.status()).toBe(201);
});
Co brakuje:
- Czy odpowiedź zawiera prawidłowy ID użytkownika?
- Czy użytkownik faktycznie istnieje w bazie danych?
- Czy użytkownik może zalogować się na utworzone konto?
Pytanie przeglądowe: Czy ta asercja dowodzi, że operacja się powiodła, czy tylko że API zwróciło oczekiwany kod statusu?
Naprawa: Dodaj silniejsze asercje:
test('stwórz użytkownika', async ({ request }) => {
const response = await request.post('/api/users', {
data: { email: 'user@example.com', name: 'Test User' }
});
expect(response.status()).toBe(201);
const body = await response.json();
expect(body.id).toBeDefined();
expect(body.email).toBe('user@example.com');
// Zweryfikuj, że użytkownik istnieje w bazie danych
const fetchResponse = await request.get(`/api/users/${body.id}`);
expect(fetchResponse.status()).toBe(200);
});
Ryzyko 7: Brakujące przypadki brzegowe
AI generuje oczywiste przypadki testowe, ale często pomija przypadki brzegowe, które mają znaczenie w produkcji.
Przykład: Przypadki graniczne daty
Testy generowane przez AI dla „użytkownik musi mieć 18 lat lub więcej”:
test('użytkownik powyżej 18 lat może się zarejestrować', async ({ page }) => {
await page.fill('[data-testid="birthdate"]', '2000-01-01'); // 26 lat
await page.click('[data-testid="submit"]');
await expect(page.locator('[data-testid="success"]')).toBeVisible();
});
test('użytkownik poniżej 18 lat nie może się zarejestrować', async ({ page }) => {
await page.fill('[data-testid="birthdate"]', '2010-01-01'); // 16 lat
await page.click('[data-testid="submit"]');
await expect(page.locator('[data-testid="error"]')).toBeVisible();
});
Co brakuje:
- Użytkownik dokładnie 18 lat (przypadek graniczny)
- Użytkownik urodzony 29 lutego (przypadek brzegowy roku przestępnego)
- Użytkownik z rokiem urodzenia 1900 (walidacja minimalnej daty)
- Przyszłe daty urodzenia (błąd walidacji)
Pytanie przeglądowe: Jakie przypadki brzegowe pomyślałby tester ludzki, które AI może przegapić?
Checklista przeglądu
Użyj tej checklisty do przeglądu każdego testu generowanego przez AI przed zmergowaniem:
1. Weryfikacja pokrycia
- Czy ten test faktycznie waliduje zachowanie, które twierdzi, że testuje?
- Jeśli usunę asercję, czy test zawodzi?
- Czy asercja sprawdza warunek, który jest zawsze prawdziwy (halucynowane pokrycie)?
2. Testy negatywne
- Czy są odpowiadające testy negatywne (przypadki błędów, nieprawidłowy input, przypadki brzegowe)?
- Czy zestaw testów pokrywa „co powinno zawieść” oprócz „co powinno się udać”?
3. Stabilność selektorów
- Czy test używa stabilnych selektorów (
data-testid, semantyczny HTML, atrybuty ARIA)? - Czy ten test zepsuje się, jeśli design UI się zmieni (klasy CSS, layout)?
4. Niezależność danych
- Czy test tworzy własne dane, czy zależy od współdzielonych/istniejących danych?
- Czy ten test może działać równolegle z innymi testami bez konfliktów?
5. Deterministyczne oczekiwania
- Czy test używa arbitralnych oczekiwań (
waitForTimeout), czy czeka na obserwowalny stan? - Czy wszystkie oczekiwania są konieczne, czy niektóre są zbędne (auto-waiting Playwright)?
6. Siła asercji
- Czy asercje dowodzą, że operacja się powiodła, czy tylko że API odpowiedziało?
- Czy są dodatkowe weryfikacje, które zwiększyłyby pewność (np. sprawdź stan bazy danych)?
7. Przypadki brzegowe
- Czy zestaw testów pokrywa warunki graniczne (min/max wartości, puste inputy, znaki specjalne)?
- Czy są przypadki brzegowe daty/czasu (lata przestępne, strefy czasowe, DST)?
- Czy są przypadki brzegowe współbieżności (race conditions, lock contention)?
8. Czytelność
- Czy test jest łatwy do zrozumienia?
- Czy nazwy zmiennych są jasne?
- Czy struktura testu jest logiczna (arrange, act, assert)?
Przykład z rzeczywistości: Przegląd testu generowanego przez AI
Test generowany przez AI:
test('przepływ checkout', async ({ page }) => {
await page.goto('http://localhost:3000/koszyk');
await page.click('button.checkout-btn');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="card"]', '4242424242424242');
await page.click('button[type="submit"]');
await page.waitForTimeout(5000);
await expect(page.locator('.success')).toBeVisible();
});
Wyniki przeglądu:
- ❌ Kruche selektory:
button.checkout-btn,input[name="email"],.success - ❌ Hardkodowane dane: Zakłada, że koszyk ma przedmioty
- ❌ Arbitralne oczekiwanie:
waitForTimeout(5000) - ❌ Słaba asercja: Sprawdza tylko, czy komunikat sukcesu jest widoczny, nie czy zamówienie zostało stworzone
Zrefaktorowany test:
test('przepływ checkout', async ({ page, request }) => {
// Seeduj dane testowe
const cart = await request.post('/api/test/seed-cart', {
data: { productId: 123, quantity: 1 }
});
await page.goto('/koszyk');
await page.click('[data-testid="checkout-button"]');
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="card-number-input"]', '4242424242424242');
await page.click('[data-testid="submit-button"]');
// Czekaj na deterministyczny stan
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
// Zweryfikuj, że zamówienie zostało stworzone
const orders = await request.get('/api/test/orders');
expect(orders.length).toBeGreaterThan(0);
});
Rezultat: Stabilny, deterministyczny i kompleksowy.
Podsumowanie
Testy generowane przez AI wymagają przeglądu przez człowieka. Często zawierają:
- Halucynowane pokrycie (przechodzące testy, które nie weryfikują zachowania)
- Brakujące testy negatywne (tylko happy paths)
- Kruche selektory (klasy CSS, kruche lokatory)
- Hardkodowane dane (współdzielony stan, niestabilne testy)
- Arbitralne oczekiwania (timeouty zamiast sprawdzeń stanu)
- Słabe asercje (kody statusu bez weryfikacji)
- Brakujące przypadki brzegowe (warunki graniczne, specjalne inputy)
Użyj checklisty przeglądu z tego postu dla każdego testu generowanego przez AI. Traktuj output AI jako pierwszy szkic, nie produkt końcowy.
Celem nie jest unikanie AI — to używanie AI bezpiecznie. AI przyspiesza tworzenie testów, ale przegląd przez człowieka zapewnia, że testy są niezawodne, łatwe w utrzymaniu i wartościowe.
:::tip[AI + Przegląd przez człowieka = Najlepsze wyniki] Najlepsze zestawy testów łączą prędkość AI (generowanie boilerplate, ujawnianie przypadków brzegowych) z ludzkim osądem (priorytetyzacja, ocena ryzyka, kontekst biznesowy). Nie używaj AI samodzielnie. Nie pisz wszystkiego ręcznie. Połącz oba. :::
Zadanie na ten tydzień: Wybierz jeden test generowany przez AI z Twojego zestawu. Przejdź przez checklistę przeglądu z tego postu. Zidentyfikuj co najmniej trzy ulepszenia. Zrefaktoryzuj test. Zmierz poprawę stabilności i pokrycia.
To kończy serię AI w QA. Więcej o architekturze testów zobacz seria Architektura testów.