AI asysta testów – użycie bramki testowej dla testów jednostkowych, aby kontrolować kod wygenerowany przez AI

Udostępniam pełny przepływ pracy AI asysta testów, od dokumentu projektowego do kodu przypadków testowych, używając bramki testowej jednostkowej, aby kontrolować ryzyko kodu wygenerowanego przez AI, przenosząc obiekt podejrzliwości z kodu na dokument przypadków testowych, aby niepowodujące przypadki testowe pilnowały nieuważności i błędów AI, a programiści mieli pewność podczas vibe codingu.

AI asysta testów rozwiązuje nie problem pokrycia testami, ale moje pytanie o zaufanie do kodu wygenerowanego przez AI. AI w ciągu trzech minut może wyprodukować duży fragment kodu, a ja potrzebuję dwóch godzin, żeby go dokładnie przejść. Trzy minuty na generowanie, dwie godziny na review nie są przesadą. Moim rozwiązaniem jest inne podejście do review: zamiast czytać kod linia po linii, czytam dokument przypadków testowych i buduję w lokalnym środowisku deweloperskim barierę testową jednostkową, która ma pilnować za mnie nagłe opóźnienia i błędy AI.

Głównym celem walki z AI asystą testów jest mały błąd

Błędy można podzielić na dwie kategorie: na łatwe i na trudne.

Trudne błędy zwykle wynikają z defektów w architekturze lub w projekcie rozwiązania, a następnie przekształcają się w długotrwałe problemy historyczne, które mogą wpłynąć na cały system. Mogą też powstać z powodu działań stron trzecich, np. bibliotek, systemów operacyjnych czy kompilatorów; do ich rozwiązania potrzeba bardziej specjalistycznej wiedzy i głębokiego zbadania. Tego typu złożone problemy nie są przedmiotem tego artykułu.

Ten artykuł dotyczy natomiast małych błędów w cyklu życia produktu, korzystając z AI przy tworzeniu przypadków testowych, aby rozwiązać następujące problemy:

  • błędów, które można szybko wykryć
  • niskopoziomowych błędów, które można naprawić niewielkimi zmianami
  • błędów wprowadzonych przez modyfikację
  • błędów wynikających z pośpiechu podczas pilnych zmian
  • kosztów i efektywności balansowania testów w ramach testów wrażliwych

Większość błędów to jednak małe błędy, ale mały błąd może doprowadzić do poważnych problemów. Rozwiązanie małego błędu lokalnie może zaoszczędzić dużo późniejszej energii.

Przenosimy obiekt podejrzliwości z kodu na dokument przypadków testowych

Próbowałem kiedyś najbardziej odważnego pomysłu: pozwolić AI w pełni zrealizować wymaganie i całkowicie nie sprawdzać generowanego przez nie kodu, podobnie jak w trybie Copilot Agent](/blog/agent-mode-experience/) całkowicie się powierzyć. Później go odrzuciłem. Zasadniczym powodem była brak pewności. Dla dobrze zaprojektowanego, logicznego programu binarnego, w którym wszystko jest jasno określone od zera do jedynki, brak pewności oznaczał utratę zaufania od góry do dołu oraz utratę zaufania klienta. Więc uczyłem się myślenia o vibe codingu: dla kodu generowanego przez AI zawsze muszę mieć pewność.

Problem wkracza na teren kosztów review. AI czasem generuje albo modyfikuje dużo kodu; trzy minuty na generowanie, dwie godziny na review to nie przesadą. Praca nad kodem testowym zwykle przekracza pracę nad kodem biznesowym i nie zawsze jest uwzględniana w ocenie dostarczeń, dlatego wielu nie chce pisać testy. Ale asystent AI jest o wiele bardziej inteligentny niż ja – to fakt. Lepiej się tego nie bóć, tylko ustawić mu barierę.

Przeniesienie obiektu podejrzliwości z kodu na dokument przypadków testowych to klucz do ominięcia tego impasu. W przeciwieństwie od kodu biznesowego, wygenerowane przez AI przypadki testowe zazwyczaj opisują bardzo konkretny scenariusz, są łatwe do przejścia i walidacji. Wielu przypadków nie trzeba nawet czytać linia po linii, wystarczy poczekać na błąd i potem przyjrzec.

---
title: Od sprawdzania kodu do sprawdzania dokumentu
---
flowchart TB
    A[AI generuje dużo kodu] e1@--> B[Koszt sprawdzania liniami]
    B e2@--> C{Kogo sprawdzić}
    C e3@-->|kod| D[Dwie godziny sprawdzania, brak pewności]
    C e4@-->|dokument testowy| E[Łatwy do sprawdzenia, szybka walidacja]
    E e5@--> F[Brama testowa jednostkowa]
    F e6@--> G[Niepowodzenie przypadku testowego ujawnia problem]

    classDef risk fill:#FFF3E0,stroke:#E65100,color:#E65100;
    classDef ok fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
    classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
    class D risk;
    class E,F,G ok;
    class e1,e2,e3,e4,e5,e6 animate;

Fundament metodologiczny: Six Sigma i DMAIC

Six Sigma to metodyka doskonalenia jakości oparta na danych; jej celem podstawowym jest zmniejszenie zmienności procesu i kontrolowanie wskaźnika defektów na poziomie 3,4 na milion, czyli 6σ1.

Oferuje ona zestaw wykonalnych procesów DMAIC:

  • Define (Definiowanie): analiza wymagań, transformacja na model danych, projektowanie struktur danych i interfejsów
  • Measure (Pomiar): monitorowanie przypadków testowych, pokrycie, wydajność, bezpieczeństwo
  • Analyze (Analiza): analiza przypadków testowych i kodu biznesowego
  • Improve (Doskonalenie): doskonalenie przypadków testowych i kodu biznesowego
  • Control (Kontrola): automatyczna brama testowa

Odpowiednimi narzędziami technicznymi są linter, statyczne i dynamiczne analizy oraz frameworki testowe. Wiele również metryk jakości, na przykład złożoności (metryka cykliczna, metryka poznawcza, głębokość poziomu zagnieżdżenia, złożoność klas), jakości (wykrywanie duplikacji, długość funkcji, magiczne liczby) oraz testów (pokrycie jednostkowe, skuteczność testów integracyjnych, skuteczność przypadków, pokrycie wyjątków).

Metodyka i narzędzia pomagają w wykrywaniu i rozwiązywaniu problemów, ale w zamian mogą nieznacznie zwiększyć koszty dostarczenia i zmniejszyć efektywność. W ograniczonych zasobach trzeba wybrać metodę, z której w krótkim, średnim i długim terminie można korzystać. Moim wyborem jest użycie testów jednostkowych jako narzędzi pomocniczych w procesie dewelowania.

Dzięki AI obsługującemu konwersje między różnymi językami ról programiści mogą połączyć zalety TDD, BDD i ATDD, szybko generować tabele przypadków testowych i kod testowy, poprawiając jakość dostarczenia.

Testowanie z inspiracją TDD/BDD/ATDD i kodem testowalnym

Różnice między trzema stylami testowania z inspiracją wynikają się następująco:

| Rzeczownik | TDD | BDD | ATDD | | Źródło sterowania | Jednostka testów | Scenariusz zachowania użytkownika | Warunki akceptacji | | Uczestnicy | Programiści | Biznes i programiści | Biznes, testy i programiści | | Poziom testu | Test jednostkowy | Testy integracyjne / end-to-end | Testy akceptacyjne | | Przykłady narzędzi | JUnit, pytest | Cucumber, SpecFlow | FitNesse, Robot Framework | | Typowy wynik | Raport z pokrycia kodu | Wykonalny dokument scenariusza użytkownika | Wskaźnik przejść testów akceptacyjnych | | Język | assert add(2,3) == 5 | Scenariusz w języku naturalnym (Given-When-Then) | Język biznesu (tabela warunków akceptacji) |

Kod testowalny opiera się na czterech podstawowych zasadach: odpowiedzialność jednostkowa (każda funkcja robi tylko jedno zadanie), iniekcja zależności (zależności przekazywane przez parametry), izolacja interfejsów (jasne typy wejścia i wyjścia), stan obserwowalny (udostępniane są interfejsy zapytań do walidacji stanu wewnętrnego).

W promptach dla AI zalecam pisać bezpośrednio rozszerzalny, odpowiedzialny jednostkowo, zależny od wstrzyknięcia. Próbowałem kiedyś kontrolować długość generowanych funkcji i plików; jego przestrzeganie było słabe. Ale po przejściu na odpowiedzialność jednostkową i iniekcję zależności wygenerowany kod był wyraźnie zgodny z oczekiwaniami. Zbyt wczesna optymalizacja to źródło wielu złych praktyk; podczas generowania podstawowych struktur danych i interfejsów wystarczy, że instruktor AI preferuje rozszerzalność.

Przykład kodu ułatwiającego testowanie:

// user.h
typedef struct {
    int id;
    char name[50];
    int age;
} User;

// Abstrakcja interfejsu magazynowania pamięci
typedef struct {
    User* (*get_user)(int id);
    int (*save_user)(const User* user);
} UserStorage;

// Utwórz użytkownika (zależność wstrzykiwana: interfejs magazynu)
User* create_user(const char* name, int age, UserStorage* storage) {
    if(strlen(name) >= 50 || age <= 0) return NULL;

    User* new_user = malloc(sizeof(User));
    new_user->id = generate_user_id();
    strncpy(new_user->name, name, 49);
    new_user->name[49] = '\0';
    new_user->age = age;

    if(storage->save_user(new_user) != 0) {
        free(new_user);
        return NULL;
    }
    return new_user;
}

Przykład kodu nieułatwiającego testowanie:

// Sztywna korelacja z operacją bazy danych
void save_user_to_db(User* user) {
    MYSQL conn;
    mysql_init(&conn);
    if(!mysql_real_connect(&conn, "localhost", "root", "pass", "test", 0, NULL, 0)) {
        log_error("DB connection failed"); // Zależność z logowaniem utrudnia testowanie
        exit(1); // Bezpośrednie zakończenie wpływa na wykonanie testu
    }
    // Złączenie SQL i zapis do bazy danych
}

Dobrze napisany kod jest cichy, zły jest wyraźny. Dobry kod ma jasne wartości zwracane, znormalizowane obsługiwanie błędów, wyraźne parametry, izolację skutków ubocznych i wspiera iniekcję zależności.

Kod udawany przez AI czasem nie jest najlepszym rozwiązaniem, a jego styl ma problemy. Chociaż często potrafi rozwiązać problem, projekt rzeczywisty to nie rynek jednorazowego zakupu, trzeba uwzględnić późniejszą konserwację. Podczas instruowania AI generowania kodu należy podobnie podkreślić czytelność jako priorytet, rozszerzalność, odpowiedzialność jednostkowa, iniekcja zależności. Dalszej dyskusji o stylu kodu nie będzie, można się odwołać do przewodnika stylu Google2.

Pułapki i kompromisy w praktycznym rozwoju

Wymagania podczas przekazywania między rolami przechodzą wiele rund konwersji językowych, każda może powodować nieścisłości. Na przykład zdanie klienta „sprawdźcie to wspólnie” może zostać zrozumiane przez backend jako „pojedynczy endpoint zwraca”. Dzięki AI generującemu szablony językowe dla różnych ról można zmniejszyć straty informacyjne podczas ręcznej konwersji.

Świadome niedopatrzenie to często źródło wielu błędów. Dzięki AI projektującemu scenariusze testowe może ono nie tylko wykonać oczekiwane zadanie, ale także nieoczekiwanie udzielić inspiracji i uzupełnić szczegóły, których wydający release nie uwzględnił. W każdej rundzie konwersji językowej można rozmawiać z AI, aby uzupełnić lukę. Markdown jest językiem, który AI najłatwiej rozumie; input i output powinny być raczej formatowane jako Markdown.

W kodzie przypadków testowych jest kilka kwestii, które trzeba uważać: izolacja przypadków, dokładność symulacji zależności, skuteczność asercji, wartości brzegowe, obsługa wyjątków, wydajność, równoległość. AI może być nieuważne i nie generować od razu wszystkich przypadków. Jednorazowe generowanie całego kodu testowego jest dla modeli bardziej wymagające, można go generować partiami.

Na przykład model Claude w Cursor radzi sobie z częścią zaleceń dotyczących kodu przypadków testowych, ale często zapomina o izolacji przypadków i scenariuszach wyjątkowych. Dodanie jednego user rule może nieco zmniejszyć ryzyko opóźnienia: przy generowaniu przypadków testowych zwróć uwagę na dokument przypadków testowych oraz izolację przypadków. Jednak nadal nie generuje wszystkich scenariuszy naraz; normalne ścieżki, ścieżki wyjątkowe, wartości brzegowe, wydajność i równoległość wymagają dodatkowych rozmów.

Ażurowa, wykonalna ścieżka pracy AI asysta testów

W połączeniu z dokumentem projektowym można utworzyć szynę od scenariusza do przypadku testowego:

---
title: Przepływ pracy AI asysta testów
---
flowchart TB
    A[Dokument projektowy] e1@--> B[Generuj scenariusz testowy do akceptacji]
    B e2@--> C[Generuj strukturę danych i interfejs]
    C e3@--> D[Generuj tabelę przypadków testowych]
    D e4@--> E[Generuj kod przypadku testowy]
    D e5@--> F[Generuj kod biznesowy]
    E e6@--> G[Lokalna brama: przypadki testowe przechodzą]
    F e7@--> G
    G e8@--> H{Czy istnieją przypadki testowe niepowodzenia?}
    H e9@-->|Tak| E
    H e10@-->|Nie| I[Prześlij do testów]

    classDef input fill:#E3F2FD,stroke:#1565C0,color:#0D47A1;
    classDef gen fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
    classDef gate fill:#FFF3E0,stroke:#E65100,color:#E65100;
    classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
    class A input;
    class B,C,D,E,F gen;
    class G,H,I gate;
    class e1,e2,e3,e4,e5,e6,e7,e8,e9,e10 animate;

Sformułowania promptów dla poszczególnych kroków można ułożyć tak: na podstawie dokumentu projektu wygeneruj scenariusz testowy do akceptacji; na podstawie scenariusza testowego wygeneruj strukturę danych i interfejs; na podstawie scenariusza testowego i interfejsu wygeneruj tabelę przypadków testowych; na podstawie interfejsu i tabeli przypadków testowych wygeneruj kod przypadków testowych; na podstawie scenariusza testowego wygeneruj kod biznesowy i zapewnij, że przypadki testowe przechodzą.

Tutaj istnieje ryzyko: AI może równocześnie modyfikować kod biznesowy na podstawie kodu testowego albo odwrotnie. Jeśli oba mają błędy, AI może się pogubić, a przy kolejnych rozmowach kod testowy i biznesowy będą się coraz bardziej mieszać; wtedy konieczna jest interwencja człowieka.

Nadmiernie ambitny prompt to przykład: nakazanie AI w jednym żądaniu pokrycia normalnego, wyjątkowego, wartości brzegowych, równoległości i wygenerowania kodu. Taki żądanie może być trudne do wykonania. Dobrym rozwiązaniem jest podzielenie scenariuszy i generowanie ich partiami za pomocą wielu zapytań w Cursor. Przypadki testowe wydajnościowe wymagają dużej ilości danych i powinny trafić na etap testów systemowych; najpierw poproś AI o napisanie generatora losowych danych, a następnie na ich podstawie zbuduj przypadki testowe.

Testy integracyjne wymagają zbudowania środowiska testowego. Niektóre przypadki zależą od zewnętrznych narzędzi; na przykład mikrosegregacja może korzystać z iptables i ipset, a kontener testowy można zbudować za pomocą Docker. Zasada brzmi: jak osoba testuje, AI musi zaimplementować identyczne kroki automatyzacji. W sprawach testów bezpieczeństwa obecnie głównie opiera się na doświadczeniu i analizie; nie ma jeszcze dojrzałych doświadczeń pomocnych AI.

Podsumowanie

Ta ścieżka pracy w porównaniu z nieużywaną przez AI generuje więcej dokumentów i kodu testowego, trudno je przejść linia po linii. Ale dokumenty są łatwiejsze do przejrzania niż kod, szczególnie tabele przypadków testowych, które mają precyzyjne znaczenie dla generowania przypadków. W praktycznym rozwoju czasem sprawdzam tylko przypadki testowe, które nie powodują błędu, i przez debugowanie takich przypadków potwierdzam problem.

Trzy obszary poprawy: podprocesy w etapie projektowania mogą się przecinać, scenariusze testowe do akceptacji mogą zależeć od struktury danych i interfejsów; tabela przypadków testowych powinna być interesująca właściwie zespołem rozwojowym; przed przesłaniem do testów trzeba ponownie wygenerować szczegółowy scenariusz testowy z uwzględnieniem szczegółów kodu biznesowego.

Ostateczny zysk nie leży w liczbie pokrycia testami, lecz w zaufaniu: dzięki tej lokalnej bramie testowej jednostkowej w końcu mogę mieć pewność co do kodu wygenerowanego przez AI, zamiast bez zastanowień wysyłać kod wyprodukowany przez „vibe coding” do produkcji.


  1. Definicja i metoda DMAIC dla Six Sigma znajdują się w słowniku Wikipedia. ↩︎

  2. Google Style Guide to publiczny przewodnik stylu kodu Google. ↩︎