Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Test Automation - automatyczne kontrole jakości

Masz komplet testów: jednostkowe, integracyjne, E2E, z atrapami i raportem pokrycia. Ale uruchamiasz je ręcznie. Wystarczy jeden pośpieszny wieczór, żeby ktoś wypchnął zmianę bez sprawdzenia - i cały ten arsenał okaże się bezużyteczny.

Legion nie polega na pamięci wartowników. Ustawia warty stałe: kontrola odbywa się sama, o wyznaczonej porze, niezależnie od tego, kto akurat pełni służbę. Tym właśnie jest automatyzacja testów - przeniesieniem odpowiedzialności z człowieka na maszynę.

Warta stała - testy przy każdej zmianie

Najprostsza warta uruchamia testy przy każdym wypchnięciu kodu:

1# .github/workflows/ci.yml
2name: CI
3on: [push, pull_request]
4
5jobs:
6  test:
7    runs-on: ubuntu-latest
8    steps:
9      - uses: actions/checkout@v4
10      - run: npm ci
11      - run: npm run test
12      - run: npm run test:e2e

Czyta się to jak rozkaz dzienny.

on: [push, pull_request]
wyznacza moment kontroli.
npm ci
instaluje zależności ściśle według
package-lock.json
- w przeciwieństwie do
npm install
niczego nie aktualizuje, więc warta sprawdza dokładnie te wersje, które zapisałeś. Potem lecą testy.

Kluczowe jest to, czego w tym pliku nie widać: każde polecenie musi zwrócić zero. Jeśli

npm run test
zakończy się błędem, warta podnosi alarm i zmiana nie wejdzie do gałęzi głównej. Cała moc automatyzacji sprowadza się do tego jednego kodu wyjścia.

Zielona warta, która kłamie

I tu dochodzimy do najważniejszej rzeczy w tej lekcji. Zautomatyzowany test jest wart tyle, ile jego rzetelność - a test asynchroniczny potrafi przechodzić, nie sprawdzając niczego.

Spójrz na te dwie wersje:

1// ŹLE - test kończy się, zanim obietnica się rozstrzygnie
2it('znajduje legionistę', () => {
3  service.findById(1);
4});
5
6// DOBRZE - test czeka na wynik
7it('znajduje legionistę', async () => {
8  const legionary = await service.findById(1);
9  expect(legionary.name).toBe('Marek');
10});

W pierwszej wersji funkcja testowa kończy się natychmiast po wywołaniu metody. Jest uznaje test za zaliczony, bo nic nie rzuciło wyjątku w tym momencie - a metoda dopiero zaczyna pracę. Gdyby zwróciła złe dane albo padła sekundę później, nikt się nie dowie.

Reguła jest krótka i bezwyjątkowa: test dotykający kodu asynchronicznego musi być

async
, a każde wywołanie zwracające obietnicę - poprzedzone
await
. Zapomniany
await
to najczęstsza przyczyna zielonych pipeline'ów nad zepsutym kodem.

Sprawdzanie wyjątków - wariant synchroniczny

Testujemy nie tylko powodzenia. Gdy metoda ma rzucić wyjątek, potrzebujemy asercji, która to potwierdzi:

1it('odrzuca ujemny żołd', () => {
2  expect(() => service.validatePay(-100)).toThrow();
3});

Zwróć uwagę na kształt argumentu: do

expect
przekazujemy funkcję, a nie wynik jej wywołania. Gdybyśmy napisali
expect(service.validatePay(-100))
, wyjątek poleciałby natychmiast, poza kontrolą Jest, i test padłby zamiast przejść. Owinięcie w
() => ...
daje Jest możliwość wywołania metody samemu i przechwycenia tego, co z niej wypadnie.

toThrow()
bez argumentu akceptuje dowolny wyjątek. Możesz zawęzić:
toThrow(BadRequestException)
sprawdzi typ, a
toThrow('Żołd nie może być ujemny')
- treść komunikatu.

Sprawdzanie wyjątków - wariant asynchroniczny

Metoda asynchroniczna nie rzuca wyjątku - zwraca odrzuconą obietnicę. Dlatego

toThrow()
z poprzedniej sekcji tu nie zadziała; potrzebna jest przystawka
rejects
:

1it('rzuca NotFoundException dla nieznanego id', async () => {
2  jest.spyOn(repo, 'findOne').mockResolvedValue(null);
3
4  await expect(service.findById(999)).rejects.toThrow(NotFoundException);
5});

Prześledźmy tę asercję po kolei, bo składa się z czterech elementów w ustalonej kolejności.

await expect(
otwiera oczekiwanie - i to
await
na początku jest tym, o czym zapomina się najczęściej; bez niego test skończy się przed rozstrzygnięciem obietnicy i znów przejdzie fałszywie. Dalej
service.findById(999)
- tutaj nie owijamy wywołania w funkcję, bo obietnica jest zwykłą wartością, którą można przekazać. Potem
.rejects
przełącza asercję na odrzucenie, a
.toThrow(NotFoundException)
sprawdza typ.

Kolejność pracy przy takim teście jest zawsze ta sama: ustaw atrapę tak, by wywołała błąd, wywołaj metodę, użyj

rejects.toThrow()
, a na końcu doprecyzuj typ i komunikat. Tutaj atrapa repozytorium zwraca
null
, co zmusza serwis do rzucenia
NotFoundException
- sprawdzamy więc obsługę błędu, nie sam błąd bazy.

Bramka lokalna - zanim kod wyjdzie z obozu

Warta w CI wyłapuje wszystko, ale odpowiada po kilku minutach. Szybszą kontrolę stawiamy na własnej maszynie:

1# .husky/pre-commit
2npm run lint
3npm run test -- --onlyChanged

Husky podpina ten skrypt pod

git commit
- gdy któreś polecenie zwróci błąd, commit się nie wykona. Flaga
--onlyChanged
każe Jest uruchomić wyłącznie testy związane ze zmienionymi plikami, więc bramka trwa sekundy, a nie minuty.

Zauważ podział ról: hak lokalny jest szybki i wybiórczy, warta w CI - wolna i kompletna. Hak ma łapać oczywiste potknięcia, zanim zajmą czyjś czas; nie zastępuje pełnego przebiegu. I to polecam, @name: nie wsadzaj do haka wszystkich testów, bo pierwsze, czego nauczy się zespół, to

--no-verify
.

Podsumowanie

Warty rozstawione, kontrola dzieje się sama:

  • automatyzacja przenosi uruchamianie testów z człowieka na maszynę - decyduje kod wyjścia polecenia,
  • on: [push, pull_request]
    wyznacza moment kontroli,
    npm ci
    instaluje dokładnie wersje z
    package-lock.json
    ,
  • test dotykający kodu asynchronicznego musi być
    async
    i używać
    await
    - bez tego przechodzi, nie sprawdzając niczego,
  • wyjątek synchroniczny:
    expect(() => metoda()).toThrow()
    - do
    expect
    idzie funkcja, nie jej wynik,
  • toThrow()
    przyjmuje typ wyjątku lub fragment komunikatu, gdy chcesz zawęzić sprawdzenie,
  • odrzucona obietnica:
    await expect(metoda()).rejects.toThrow(NotFoundException)
    - tu przekazujemy samą obietnicę, bez owijania w funkcję,
  • kolejność: ustaw atrapę na błąd, wywołaj metodę,
    rejects.toThrow()
    , zweryfikuj typ i komunikat,
  • hak
    pre-commit
    z
    --onlyChanged
    daje szybką bramkę lokalną; pełny przebieg zostaw dla CI.

W następnej lekcji zmierzysz się z projektem - kompleksowym otestowaniem systemu zarządzania flotą. A na razie zapamiętaj: warta stała jest warta tyle, ile rzetelność testów, które uruchamia - a test bez

await
melduje spokój, choć nikt nie sprawdzał bram.

Przejdź do CodeWorlds