Integracja testów
Twój przepływ checkoutu przechodzi każdy test jednostkowy, plakietka pokrycia mówi 94%, a mimo to zwraca 500 na produkcji, bo nic nigdy nie przećwiczyło ścieżki, na której dostawca płatności przekracza limit czasu w trakcie żądania. Testy były zielone. Testowały też nie to, co trzeba — głównie Twoje mocki sprawdzające, że Twoje mocki zostały wywołane. To, czego potrzebujesz, to zestaw, który wychwytuje błędy integracyjne, a nie ściana zielonych ptaszków, które Cię okłamują.
Tutaj agent kodujący AI zarabia na swoje utrzymanie. Nie przez generowanie setki trywialnych testów expect(sum(1,2)).toBe(3), ale przez czytanie Twojego prawdziwego kodu, znajdowanie trybów awarii, które pominąłeś, i pisanie testów, które nie przechodzą z właściwych powodów. Ten artykuł pokazuje ten przepływ pracy w Cursorze, Claude Code i Codeksie.
Co zdobędziesz
Dział zatytułowany „Co zdobędziesz”- Pętlę TDD, w której agent najpierw pisze test, który nie przechodzi, a potem implementację — żebyś wiedział, że test naprawdę potrafi nie przejść
- Wielokrotnie używalny prompt, który generuje testy integracyjne dla trasy Express + Drizzle, włącznie ze ścieżką awarii bazy danych potwierdzającą prawdziwe
503 - Prompt do E2E w Playwright, który przeżywa w CI zamiast sypać się przy trzecim uruchomieniu
- Powtarzalny sposób naprawiania niestabilnych testów przez atakowanie przyczyny źródłowej, a nie zaklejanie jej
sleep() - Bezgłowe polecenie testowe
claude -poraz przepis.claude/commands, który możesz wrzucić do prawdziwego repozytorium już dziś
Przykład przewodni
Dział zatytułowany „Przykład przewodni”Przez cały artykuł będziemy testować jeden prawdziwy endpoint: trasę Express, która tworzy zamówienie, opartą na Drizzle ORM na Postgresie. Nic tu nie jest zabawką — ma dwie rzeczy, które psują się na produkcji: zewnętrzne wywołanie (dostawca płatności) i zapis do bazy danych, który może się nie powieść.
import { Router } from 'express';import { db } from '../db';import { orders } from '../db/schema';import { charge } from '../lib/payments';
export const ordersRouter = Router();
ordersRouter.post('/orders', async (req, res) => { const { userId, amountCents, idempotencyKey } = req.body; if (!userId || !amountCents) { return res.status(400).json({ error: 'userId and amountCents required' }); }
try { const payment = await charge({ amountCents, idempotencyKey }); const [order] = await db .insert(orders) .values({ userId, amountCents, paymentId: payment.id, status: 'paid' }) .returning(); return res.status(201).json(order); } catch (err) { if (err instanceof PaymentError) return res.status(402).json({ error: 'payment_failed' }); // DB write failed after a successful charge -- the dangerous case return res.status(503).json({ error: 'order_persist_failed', retryable: true }); }});Interesujący test to nie „201 na ścieżce szczęśliwej”. To: obciążenie się powiodło, ale insert do bazy rzucił wyjątek — czy zwracamy 503 z możliwością ponowienia i czy unikamy podwójnego obciążenia przy ponowieniu? To właśnie ten błąd budzi Cię telefonem o 2 w nocy.
Pętla TDD, która naprawdę wychwytuje błędy
Dział zatytułowany „Pętla TDD, która naprawdę wychwytuje błędy”Dyscyplina, która sprawia, że testy generowane przez AI są godne zaufania, jest prosta: spraw, by test nie przechodził, zanim pozwolisz agentowi cokolwiek zaimplementować. Test, który nigdy nie był czerwony, nie jest testem — to komentarz. Przeprowadź agenta przez czerwony → zielony → refaktoryzacja w sposób jawny.
-
Najpierw napisz test, który nie przechodzi. Każ agentowi napisać test dla zachowania, które jeszcze nie istnieje, i zatrzymać się przed implementacją.
-
Uruchom go i potwierdź, że nie przechodzi z właściwego powodu. Nie literówka, nie brakujący import — prawdziwa porażka asercji albo 404 na trasie, której jeszcze nie zbudowałeś.
-
Zaimplementuj do zielonego. Pozwól agentowi napisać minimum kodu potrzebnego do przejścia, z wyraźną instrukcją, by nie dotykał testu.
-
Refaktoryzuj pod zielonym paskiem. Teraz zestaw jest Twoją siatką bezpieczeństwa przy porządkach.
Mechanika prowadzenia tej pętli różni się w zależności od narzędzia. Prompt jest niemal identyczny; sposób utrzymywania agenta w ryzach — już nie.
Użyj trybu Agent i oprzyj się na checkpointach. Przed krokiem implementacji test, który nie przechodzi, jest naturalnym checkpointem — jeśli agent „naprawi” test zamiast kodu (klasyczna porażka), przywróć ten checkpoint i ponów prompt.
W Composerze, mając orders.ts oraz pusty orders.test.ts w kontekście:
Write a Vitest integration test for POST /orders covering the case wherecharge() resolves but the Drizzle insert rejects. Assert a 503 with{ retryable: true }. Do NOT implement the route yet -- the test must fail.Uruchom npm run test w terminalu Cursora, zobacz, jak robi się czerwony, a potem rozpocznij nowy prompt: „Now make this pass without editing the test file.” Trzymaj włączone „Iterate on lints”, aby błędy typów były naprawiane w tej samej turze.
Claude Code błyszczy tutaj, bo możesz zabezpieczyć pętlę uprawnieniami i uruchomić ją w CI. Ogranicz pierwszy krok wyłącznie do pisania testów:
claude -p "Write a Vitest integration test for POST /orders: charge()succeeds but the Drizzle insert rejects, expect 503 with retryable:true.Do not implement the route." \ --allowedTools "Read" "Write" "Bash(npm run test*)"Ponieważ Edit na src/routes/ nie jest w --allowedTools, agent fizycznie nie może „naprawić” trasy, by przepuścić niedopracowany test — musi napisać test, który uczciwie nie przechodzi. Następnie zdejmij zabezpieczenie i uruchom krok implementacji interaktywnie.
W TUI Codeksa ustaw zatwierdzenia tak, by uruchomienie testu było automatyczne, ale edycje były widoczne:
codex --ask-for-approval on-requestPoproś go o napisanie testu, który nie przechodzi, pozwól mu uruchomić zestaw (Codex wykonuje polecenie w swoim sandboksie) i przejrzyj czerwone wyjście przed zatwierdzeniem diffu implementacji. Dla bezobsługowej pętli lokalnej codex --full-auto ustawia zatwierdzanie on-request z sandboksem workspace-write — dobre, gdy już ufasz promptowi.
Testy integracyjne: testuj szwy, nie mocki
Dział zatytułowany „Testy integracyjne: testuj szwy, nie mocki”Awaria z początkowego scenariusza wydarzyła się na szwie — granicy między Twoim kodem a zewnętrzną usługą. Nadmiernie zmockowane zestawy przechodzą właśnie dlatego, że nigdy nie dotykają tych szwów. Lekarstwem jest mockowanie na krawędziach (granica HTTP dostawcy płatności, zachowanie awarii bazy danych) i uruchamianie wszystkiego pomiędzy naprawdę.
Mocny prompt do testów integracyjnych jest konkretny w trzech sprawach: co zmockować, co uruchomić naprawdę i które ścieżki błędów są obowiązkowe.
Oto kształt tego, co dobry agent produkuje dla niebezpiecznego przypadku — zwróć uwagę, że potwierdza zachowanie (status, treść, liczbę wywołań), nigdy szczegóły implementacji:
import request from 'supertest';import { describe, it, expect, vi, beforeEach } from 'vitest';import { app } from '../app';import * as payments from '../lib/payments';import { db } from '../db';
beforeEach(() => vi.restoreAllMocks());
it('returns retryable 503 when the DB write fails after a charge', async () => { vi.spyOn(payments, 'charge').mockResolvedValue({ id: 'pay_123' }); vi.spyOn(db, 'insert').mockImplementation(() => { throw new Error('connection terminated'); });
const res = await request(app) .post('/orders') .send({ userId: 'u1', amountCents: 4999, idempotencyKey: 'k1' });
expect(res.status).toBe(503); expect(res.body).toMatchObject({ retryable: true }); expect(payments.charge).toHaveBeenCalledTimes(1);});Gdzie serwery MCP zmieniają historię integracji
Dział zatytułowany „Gdzie serwery MCP zmieniają historię integracji”Jeśli na potrzeby testów integracyjnych uruchamiasz prawdziwego Postgresa zamiast mockować bazę, serwer Postgres MCP (@modelcontextprotocol/server-postgres) pozwala agentowi przejrzeć Twój żywy schemat i pisać testy zgodne z prawdziwymi ograniczeniami kolumn, a nie z jego zgadywaniem schematu. Podłącz go raz, a prompt zmienia się z „załóż schemat” na „odczytaj tabelę orders i potwierdź jej prawdziwe ograniczenia NOT NULL”. Dla E2E w przeglądarce Playwright MCP (@playwright/mcp) pozwala agentowi sterować prawdziwą stroną i czytać DOM podczas pisania testu, zamiast wymyślać selektory.
Testy end-to-end: te, które sypią się w CI
Dział zatytułowany „Testy end-to-end: te, które sypią się w CI”Testy E2E nie przechodzą w CI z jednego powodu częściej niż z jakiegokolwiek innego: test ściga się z aplikacją. Agent kliknął, zanim przycisk był interaktywny, potwierdził, zanim wywołanie sieciowe się rozwiązało, albo polegał na sztywnym waitForTimeout. Lekarstwem jest zakazanie arbitralnych oczekiwań oraz wymuszenie asercji web-first i dostępnych lokatorów.
Instrukcja „uruchom to 3 razy” na końcu wykonuje realną pracę: zamienia jednorazowe generowanie w sprawdzenie stabilności, zanim test w ogóle dotrze do Twojego potoku.
Wpinanie testów w CI
Dział zatytułowany „Wpinanie testów w CI”Korzyść z bezgłowych agentów polega na tym, że generowanie testów i triage mogą działać w CI, nie tylko na Twoim laptopie. Te trzy narzędzia idą różnymi drogami.
Cursor jest przede wszystkim IDE, więc połowę z CI stanowi Twój normalny runner testów — wartością Cursora jest tworzenie. Realistyczne zadanie GitHub Actions, które uruchamia zestaw Vitest, który Cursor pomógł Ci napisać:
name: teston: [pull_request]jobs: vitest: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 22 cache: npm - run: npm ci - run: npm run test -- --coverageUżyj Background Agent Cursora, by szkicował testy dla nowej trasy na gałęzi, podczas gdy Ty pracujesz dalej, a potem przejrzyj diff, zanim trafi do tego potoku.
Uruchamiaj Claude Code bezgłowo, by przy każdym PR triagować zestaw, który nie przechodzi, i publikować ustrukturyzowane podsumowanie. --output-format json sprawia, że wynik jest czytelny dla maszyny:
name: test-triageon: [pull_request]jobs: triage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 22 - run: npm ci - run: | claude -p "Run npm run test. If anything fails, name the failing test, the likely root cause, and the one-line fix. Do not edit files." \ --allowedTools "Bash(npm run test*)" "Read" \ --output-format json > triage.json env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}Ponieważ dozwolone są tylko Bash(npm run test*) i Read, uruchomienie triage może diagnozować, ale nigdy po cichu przepisać Twoich testów.
Codex Cloud uruchamia zadanie w izolowanym worktree, więc możesz przekazać „napisz brakujące testy dla tej trasy” bez blokowania swojej maszyny. Z terminala:
# Submit a Cloud task that writes tests in an isolated environmentcodex cloud exec --env my-ci-env \ "Add Vitest integration tests for POST /orders covering the 402, 503, and idempotency cases. Run the suite and make it green."
# When it finishes, pull the diff into your local tree to reviewcodex apply <TASK_ID>Dla lokalnego, nieinteraktywnego uruchomienia w CI codex exec "..." strumieniuje wyniki na stdout (dodaj --full-auto, by uzyskać autonomiczny sandboks workspace-write).
Wielokrotnie używalne polecenie slash do generowania testów
Dział zatytułowany „Wielokrotnie używalne polecenie slash do generowania testów”Katalog .claude/commands/ zamienia dobry prompt w jednowyrazowe polecenie. Sztuczka, którą pomija wersja generyczna: nazwij stos i obowiązkowe ścieżki błędów w treści polecenia, tak by każde wywołanie produkowało prawdziwy przepis zamiast mglistych „kompleksowych testów”.
Write Vitest + supertest integration tests for the Express route: $ARGUMENTS
Stack: Express, Drizzle ORM (Postgres), Vitest, supertest.Run the real middleware + handler. Mock only external services and the DB.
Always include these cases, one it() each:- happy path (correct status + persisted row)- input validation (400 on missing required fields)- external dependency throws (assert the mapped error status, e.g. 402)- DB write fails AFTER an external side effect (assert a retryable 5xx)- idempotency: a repeated request with the same key has no double effect
Assert status codes and specific body fields. No snapshot tests.Reset mocks in beforeEach. Run the suite and report pass/fail per case.Wywołaj je w sesji Claude Code przez /test-route POST /orders. Ta sama treść promptu działa jako zapisany prompt w Cursorze lub prompt w Codeksie — przepis jest przenośny; różni się tylko sposób wywołania.
Gdy to się psuje
Dział zatytułowany „Gdy to się psuje”Zestaw jest zielony, ale produkcja nadal się psuje. Testujesz mocki, nie szwy. Mockowanie funkcji będącej przedmiotem testu sprawia, że asercja jest błędnym kołem. Uruchom ponownie powyższy prompt integracyjny i wymuś prawdziwe wykonanie wszystkiego poza prawdziwą granicą zewnętrzną (wywołanie HTTP płatności, sterownik bazy). Jeśli Twój mock i prawdziwe API się rozjeżdżają, dodaj test kontraktowy, który co noc uderza w endpoint sandboksowy.
Agent „naprawił” niezdający test, osłabiając asercję. To najczęstsza porażka testowania z AI: poproszony o przepuszczenie testów, edytuje test zamiast kodu. Zapobiegaj temu strukturalnie — w Claude Code pomiń Edit na ścieżce testu w --allowedTools podczas kroku implementacji; w Cursorze przywróć checkpoint sprzed implementacji i ponów prompt z „without editing the test file”.
E2E przechodzi lokalnie, sypie się w CI. Prawie zawsze wyścig. Przeszukaj wygenerowany test pod kątem waitForTimeout i usuń każde trafienie, a potem poproś agenta, by zastąpił każde asercją web-first lub toHaveURL. CI jest wolniejsze niż Twój laptop, więc każde sztywne oczekiwanie, które „działa” lokalnie, to bomba zegarowa.
Testy potwierdzają implementację, nie zachowanie. Jeśli zmiana nazwy prywatnej metody psuje dwadzieścia testów, agent zbyt mocno je sprzęgnął. Prompt: „These tests break on safe refactors. Rewrite them to assert observable behavior — inputs, outputs, status codes, persisted state — never private method names or call order unless ordering is the contract.”
Pokrycie jest wysokie, ale błędy nadal trafiają na produkcję. Pokrycie mierzy wykonane linie, nie wykonane asercje. Test może uruchomić linię i niczego nie potwierdzić. Poproś agenta o test mutacyjny krytycznego modułu: „Introduce three plausible bugs in src/routes/orders.ts one at a time and tell me which tests catch each. Any bug that nothing catches reveals a missing assertion.”