Przejdź do głównej zawartości

Testowanie i zapewnianie jakości wspomagane przez AI

Twój pakiet testów przechodzi lokalnie, ale sypie się w CI. Pokrycie utknęło na 60% od miesiąca, bo nikomu nie chce się pisać nudnych przypadków. A jeden test integracyjny, który wyłapałby zeszłotygodniową regresję, nigdy nie powstał, bo skonfigurowanie przeglądarki było uciążliwe. Testowanie to dokładnie ten rodzaj masowej, wzorcowej pracy, w której agent Cursora zarabia na siebie — pod warunkiem że pokierujesz go właściwymi promptami i sam będziesz pilnować tego, co naprawdę jest asercjonowane.

  • Konfigurację Auto-Run w Cursorze, dzięki której agent uruchamia testy i iteruje bez niańczenia
  • Gotowy do skopiowania prompt TDD, który zmusza agenta do napisania testu najpierw, zobaczenia jak zawodzi, a dopiero potem implementacji
  • Prompty na przypadki, które programiści pomijają: ponawianie async, ścieżki błędów i luki sygnalizowane przez raport pokrycia
  • Konfigurację Playwright MCP do testów E2E w przeglądarce, które agent może przeprowadzić od początku do końca
  • Przepływ debugowania dla najgorszego rodzaju awarii — tej sporadycznej
  • Jasną listę miejsc, w których testy generowane przez AI zawodzą, i jak to wychwycić

Pętla TDD działa tylko wtedy, gdy agent może sam uruchamiać twoje testy, czytać błędy i próbować ponownie bez zatrzymywania się po pozwolenie przy każdym npm test. W aktualnym Cursorze znajdziesz to w Cursor Settings -> Agents -> Auto-Run (to funkcja, którą wcześniej nazywano „YOLO Mode”).

Ustaw Auto-Run Mode na Run Everything, jeśli ufasz agentowi w tym repozytorium, albo zostaw na Ask Every Time i dodaj bezpieczne polecenia do Command Allowlist, aby komendy testów i builda działały bez pytań, a polecenia destrukcyjne nadal się zatrzymywały.

Przy tak skonfigurowanym Auto-Run agent może uruchamiać pakiet, naprawiać zawodzące testy aktualizując kod źródłowy i ponawiać aż do zieleni — to rdzeń pętli TDD opisanej niżej.

Cykl Red -> Green -> Refactor czysto przekłada się na pętlę agenta: pisze zawodzący test, uruchamia go, by potwierdzić, że zawodzi z właściwego powodu, implementuje aż do zieleni, a potem refaktoryzuje z testami jako siatką bezpieczeństwa. Liczy się dyscyplina zmuszania agenta do napisania testu najpierw — inaczej pisze kod i testy razem, a testy tylko przyklepują to, co właśnie wyprodukował.

Typowy przebieg wygląda tak: agent pisze specyfikację, uruchamia ją, widzi błędy i iteruje.

describe('markdownToHtml', () => {
it('converts headings', () => {
expect(markdownToHtml('# Title')).toBe('<h1>Title</h1>');
expect(markdownToHtml('## Subtitle')).toBe('<h2>Subtitle</h2>');
});
it('escapes HTML special characters in text', () => {
expect(markdownToHtml('5 < 6 & 7 > 2')).toBe('<p>5 &lt; 6 &amp; 7 &gt; 2</p>');
});
it('handles unclosed bold markers gracefully', () => {
expect(markdownToHtml('**bold')).toBe('<p>**bold</p>');
});
});

Przypadki z escapowaniem i niezamkniętymi znacznikami to te, o których człowiek zwykle zapomina — i te, które w produkcji zamieniają się w błędy XSS albo zepsute wyjście. Nazwanie ich wprost w prompcie sprawia, że agent je pokrywa.

Agent jest najmocniejszy w testach jednostkowych, bo wzorce są dobrze ugruntowane i może je uruchamiać w ciasnej pętli. Wskaż mu istniejące testy w twoim repozytorium, żeby dopasował się do twoich konwencji, zamiast wymyślać nowe.

Write Vitest + React Testing Library tests for this UserProfile
component. Cover: renders name and avatar from props, falls back to
initials when avatar is missing, calls onEdit when the edit button is
clicked, and disables the edit button while saving is true. Follow the
query and matcher conventions in src/components/__tests__/Card.test.tsx.
describe('UserProfile', () => {
it('renders user name and avatar', () => {
render(<UserProfile user={{ name: 'Ada', avatar: '/a.png' }} />);
expect(screen.getByText('Ada')).toBeInTheDocument();
expect(screen.getByRole('img')).toHaveAttribute('src', '/a.png');
});
it('falls back to initials when avatar is missing', () => {
render(<UserProfile user={{ name: 'Ada Lovelace' }} />);
expect(screen.getByText('AL')).toBeInTheDocument();
});
it('calls onEdit when the edit button is clicked', async () => {
const onEdit = vi.fn();
render(<UserProfile user={{ name: 'Ada' }} onEdit={onEdit} />);
await userEvent.click(screen.getByRole('button', { name: /edit/i }));
expect(onEdit).toHaveBeenCalledOnce();
});
});

Operacje asynchroniczne to miejsce, gdzie zwykle kryje się luka w pokryciu

Dział zatytułowany „Operacje asynchroniczne to miejsce, gdzie zwykle kryje się luka w pokryciu”

Zielony test „happy path” dla kodu async ukrywa przypadki, które naprawdę psują się w produkcji: ponowienie, timeout, odrzucony promise. Zmuś agenta, żeby testował je wprost.

describe('fetchUserData', () => {
it('retries once on network failure, then succeeds', async () => {
fetch
.mockRejectedValueOnce(new Error('Network error'))
.mockResolvedValueOnce({ ok: true, json: async () => ({ id: 1 }) });
const result = await fetchUserData(1);
expect(fetch).toHaveBeenCalledTimes(2);
expect(result).toEqual({ id: 1 });
});
it('does not retry on a 4xx response', async () => {
fetch.mockResolvedValueOnce({ ok: false, status: 404 });
await expect(fetchUserData(1)).rejects.toThrow();
expect(fetch).toHaveBeenCalledOnce();
});
});

Do testów end-to-end sterowanych przeglądarką daj agentowi serwer Playwright MCP. Pozwala on agentowi sterować prawdziwą przeglądarką — nawigować, wypełniać formularze, klikać i asercjonować — dzięki czemu może zarówno napisać test, jak i uruchomić go na twojej lokalnej aplikacji, by potwierdzić, że naprawdę działa.

test('successful login redirects to dashboard', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('correct-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/\/dashboard/);
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('wrong-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('alert')).toContainText('Invalid credentials');
});

Lokatory dostępnościowe (getByRole, getByLabel) przetrwają refaktoryzacje CSS, które zepsułyby kruche selektory #id/.class — dlatego warto upierać się przy nich w prompcie.

Test, który zawodzi raz na dziesięć przebiegów, to tryb awarii pochłaniający najwięcej czasu, i dokładnie tu opłaca się zdolność agenta do wielokrotnego uruchamiania testu pod rząd.

W praktyce agent wyciąga na wierzch zwykłych winowajców: poprzedni test, który nie posprzątał połączeń z bazą danych, afterEach, którego nigdy nie dodano, albo asercję ścigającą się z odbitym (debounced) aktualizowaniem. Naprawą prawie zawsze jest izolacja — porządny hook teardown albo zaczekanie na ustabilizowanie się stanu — a nie dłuższy timeout.

Procent pokrycia sam w sobie to metryka próżności — 100% pokrycia linii ze słabymi asercjami i tak wypuszcza błędy. Użyj raportu, by znaleźć, które ścieżki nie są przetestowane, a potem poproś o sensowne testy dla tych ryzykownych.

File | % Stmts | % Branch | % Funcs | % Lines |
------------------|---------|----------|---------|---------|
auth/login.js | 75.00 | 66.67 | 100.00 | 75.00 |
auth/reset.js | 45.00 | 33.33 | 66.67 | 45.00 |

Raport taki jak ten mówi agentowi, gdzie celować: auth/reset.js ma najcieńsze pokrycie gałęzi, więc to przypadki brzegowe resetowania hasła (wygasły token, już użyty token, nieznany e-mail) warto napisać.

Agent edytuje asercje zamiast naprawiać kod. Gdy test zawodzi, agent czasem „naprawia” go, osłabiając asercję (toBe zmienia się w toBeDefined). Zawsze dodawaj „fix the implementation, not the test” do promptów TDD i przeglądaj diff — jeśli test stał się łatwiejszy zamiast kodu poprawnym, odrzuć go i poproś ponownie.

Testy przechodzą, ale funkcja jest zepsuta (fałszywe negatywy). Przemockowane testy asercjonują względem mocka, nie rzeczywistego zachowania. Jeśli test nigdy nie ćwiczy integracji, którą rzekomo pokrywa, daje fałszywe poczucie pewności. Prompt: „Review these tests — which ones would still pass if the implementation were deleted? Strengthen them to assert real behavior.” Zachowaj przynajmniej jeden test integracyjny lub E2E, który uderza w prawdziwe szwy.

Auto-Run robi coś destrukcyjnego. „Run Everything” jest potężne i niewybredne. Jeśli agent uruchomi polecenie, które usuwa dane albo wypycha do zdalnego repozytorium, przełącz się na Ask Every Time z Command Allowlist (zobacz politykę powyżej), aby tylko bezpieczne polecenia działały automatycznie, a wszystko inne zatrzymywało się po zatwierdzenie.

Playwright MCP nie może dosięgnąć twojej aplikacji. Testy przeglądarkowe agenta zawodzą natychmiast z błędami połączenia, gdy serwer deweloperski nie działa albo jest na innym porcie. Najpierw uruchom aplikację (npm run dev), potwierdź, że adres URL w prompcie się zgadza, i powiedz agentowi, żeby zaczekał na gotowość serwera, zanim zacznie nawigować.

Chwiejne testy „naprawione” dłuższymi timeoutami. Jeśli naprawa chwiejnego testu przez agenta to tylko większy timeout, zamaskował wyścig, a nie go rozwiązał. Poproś ponownie o źródłową przyczynę (wyciekający stan, niezaczekany promise) i nalegaj na deterministyczną poprawkę potwierdzoną 25 kolejnymi przejściami pod rząd.