Testy automatyczne - ubezpieczenie, które płaci się samo

Piątek, 16:30. Tomek wdraża poprawkę w module rabatów. Zmienił jedną linijkę kodu - rabat 10% dla klientów premium. Wdrożył na produkcję. Poszedł do domu.

Sobota, 9:00. Telefon od księgowej: "Wszystkie faktury z piątku po 16:30 mają zerową kwotę. Wystawiliśmy 47 faktur na 0 złotych."

Co się stało? Zmiana w module rabatów przypadkiem nadpisała cenę końcową zerem, gdy rabat nie miał zastosowania. Tomek testował ręcznie: wybrał klienta premium, sprawdził rabat, działa. Nie sprawdził, co się dzieje dla klienta bez rabatu. Bo "oczywiste, że to nie powinno wpłynąć".

47 korekt faktur. Telefony do klientów z przeprosinami. Weekend księgowej na poprawianiu. Reputacja firmy - nadszarpnięta.

Jeden test automatyczny zająłby 30 sekund do napisania i złapałby ten błąd zanim kod trafił na produkcję:

test "faktura bez rabatu ma poprawną kwotę" do
  klient = insert(:klient, typ: :standard)  # nie premium
  zamowienie = insert(:zamowienie, klient: klient, wartosc: 1000)

  faktura = Faktury.wystaw(zamowienie)

  assert faktura.kwota == 1000  # nie zero!
end

Ten test uruchamiałby się automatycznie przy każdym wdrożeniu. Zmiana Tomka nie przeszłaby na produkcję. Piątkowy wieczór byłby spokojny.

Czym są testy automatyczne (dla nieinformatyków)

Test automatyczny to program, który sprawdza, czy inny program działa poprawnie. Zamiast człowieka, który klika po aplikacji i sprawdza wyniki, maszyna robi to sama - szybciej, dokładniej, bez zmęczenia, za każdym razem tak samo.

Wyobraź sobie listę kontrolną pilota przed lotem:

  • Paliwo: OK
  • Klapy: OK
  • Silniki: OK
  • Podwozie: OK
  • Ciśnienie w kabinie: OK

Pilot nie lata "na czuja". Przed każdym lotem sprawdza tę samą listę. Testy automatyczne to lista kontrolna Twojego systemu - sprawdzana przed każdym wdrożeniem, automatycznie, w sekundach.

Lista kontrolna systemu (uruchamiana automatycznie):

✓ Faktura z rabatu ma poprawną kwotę
✓ Faktura bez rabatu ma poprawną kwotę
✓ Zamówienie z pustym koszykiem jest odrzucane
✓ Klient z błędnym NIP-em nie może się zarejestrować
✓ Stany magazynowe aktualizują się po wydaniu
✓ Raport sprzedaży sumuje się poprawnie
✓ Login z błędnym hasłem jest odrzucany
✓ Użytkownik bez uprawnień nie widzi danych finansowych
...
237 testów przeszło w 4.2 sekundy. Wdrożenie bezpieczne.

Dlaczego ręczne testowanie nie działa

Człowiek nie sprawdzi wszystkiego

Twój system ma 200 funkcji. Każda funkcja ma 5-10 scenariuszy (poprawne dane, błędne dane, puste pola, wartości graniczne, uprawnienia). To 1000-2000 scenariuszy do sprawdzenia.

Tomek testuje ręcznie: klika główne scenariusze, sprawdza czy strona się nie sypie. Testuje może 30 scenariuszy. 970 pozostałych? "Nie powinno się zepsuć, nic tam nie zmieniałem."

Problem: oprogramowanie to sieć zależności. Zmiana w module rabatów wpływa na moduł faktur, który wpływa na moduł raportów, który wpływa na dashboard zarządu. Tomek nie ma szans sprawdzić wszystkich powiązań ręcznie. Ale komputer ma - w 4 sekundy.

Człowiek się męczy

Poniedziałek: Tomek testuje uważnie, każdy przypadek, każde pole. Piątek: Tomek testuje "na szybko", główne ścieżki, "reszta pewnie działa".

Testy automatyczne nie mają piątków. Nie mają złych dni. Nie spieszą się na autobus. Każde uruchomienie jest identycznie dokładne.

Człowiek zapomina

Tomek 6 miesięcy temu naprawił buga: "faktura z kwotą 0 PLN gdy klient nie ma przypisanej grupy cenowej". Naprawił, poszedł dalej. Teraz ktoś zmienia moduł grup cenowych. Czy pamięta, żeby sprawdzić ten edge case? Nie. Bug wraca. To się nazywa regresja i jest najczęstszym typem błędu w oprogramowaniu.

Test automatyczny pamięta wiecznie. Napisany raz, sprawdza ten scenariusz przy każdym wdrożeniu, przez lata.

Co testy chronią w praktyce

Faktury i kwoty

To jest obszar, w którym błąd kosztuje natychmiast i konkretnie.

describe "wystawianie faktur" do
  test "kwota netto jest sumą pozycji" do
    pozycje = [
      %{nazwa: "Produkt A", ilosc: 3, cena: 100},
      %{nazwa: "Produkt B", ilosc: 1, cena: 250}
    ]

    faktura = Faktury.wystaw(pozycje, stawka_vat: 23)

    assert faktura.netto == Decimal.new("550.00")
    assert faktura.vat == Decimal.new("126.50")
    assert faktura.brutto == Decimal.new("676.50")
  end

  test "rabat procentowy oblicza się poprawnie" do
    faktura = Faktury.wystaw([%{cena: 1000, ilosc: 1}], rabat: 15)

    assert faktura.netto == Decimal.new("850.00")  # 1000 - 15%
  end

  test "rabat nie może przekroczyć 100%" do
    assert {:error, :invalid_rabat} =
      Faktury.wystaw([%{cena: 1000, ilosc: 1}], rabat: 150)
  end

  test "faktura z pustą listą pozycji jest odrzucana" do
    assert {:error, :empty_items} = Faktury.wystaw([], stawka_vat: 23)
  end

  test "zaokrąglenie groszy jest zgodne z przepisami" do
    faktura = Faktury.wystaw([%{cena: 33.33, ilosc: 3}], stawka_vat: 23)

    # 33.33 × 3 = 99.99, VAT 23% = 22.9977 → 23.00
    assert faktura.vat == Decimal.new("23.00")
  end
end

5 testów. Uruchamiają się w milisekundach. Chronią przed błędami, które kosztują godziny naprawiania, telefony do klientów i korektę faktur. Napisanie ich trwa 15 minut. Naprawianie błędów, które łapią, trwałoby dni.

Uprawnienia i bezpieczeństwo

Kto widzi co? Kto może co zmienić? Kto ma dostęp do danych finansowych?

describe "uprawnienia" do
  test "magazynier nie widzi marż" do
    user = insert(:user, role: :magazynier)
    conn = log_in(user)

    conn = get(conn, "/raporty/marze")

    assert conn.status == 403  # Forbidden
  end

  test "handlowiec widzi tylko swoich klientów" do
    handlowiec = insert(:user, role: :handlowiec)
    moj_klient = insert(:klient, opiekun: handlowiec)
    cudzy_klient = insert(:klient, opiekun: insert(:user))

    klienci = Klienci.list(as: handlowiec)

    assert moj_klient in klienci
    refute cudzy_klient in klienci
  end

  test "usunięty użytkownik nie może się zalogować" do
    user = insert(:user)
    Users.deactivate(user)

    assert {:error, :account_disabled} = Auth.login(user.email, "haslo")
  end
end

Te testy uruchamiają się przy każdym wdrożeniu. Jeśli ktokolwiek przypadkiem zmieni logikę uprawnień - testy zatrzymają wdrożenie. Nie dowiesz się o problemie z uprawnień od UODO przy kontroli RODO - dowiesz się od testu w CI, zanim kod trafi na produkcję.

Stany magazynowe

Najczęstsze źródło konfliktów między magazynem a systemem.

describe "stany magazynowe" do
  test "wydanie zmniejsza stan" do
    produkt = insert(:produkt, stan: 100)

    {:ok, _} = Magazyn.wydaj(produkt, ilosc: 3)

    assert Magazyn.stan(produkt) == 97
  end

  test "nie można wydać więcej niż jest na stanie" do
    produkt = insert(:produkt, stan: 2)

    assert {:error, :insufficient_stock} = Magazyn.wydaj(produkt, ilosc: 5)
    assert Magazyn.stan(produkt) == 2  # stan nie zmieniony
  end

  test "dwa jednoczesne wydania nie powodują ujemnego stanu" do
    produkt = insert(:produkt, stan: 1)

    # Symulacja wyścigu - dwa procesy próbują wydać ostatnią sztukę
    task1 = Task.async(fn -> Magazyn.wydaj(produkt, ilosc: 1) end)
    task2 = Task.async(fn -> Magazyn.wydaj(produkt, ilosc: 1) end)

    results = [Task.await(task1), Task.await(task2)]

    assert Enum.count(results, &match?({:ok, _}, &1)) == 1
    assert Enum.count(results, &match?({:error, _}, &1)) == 1
    assert Magazyn.stan(produkt) == 0  # nie -1!
  end
end

Trzeci test jest kluczowy: sprawdza, czy dwie osoby próbujące wydać ostatnią sztukę jednocześnie nie spowodują ujemnego stanu. To bug, który przy ręcznym testowaniu jest niemal niemożliwy do złapania - bo musisz kliknąć w dwóch oknach w tej samej milisekundzie. Test automatyczny symuluje to łatwo.

Logika biznesowa z edge case'ami

Reguły biznesowe w firmach bywają skomplikowane i pełne wyjątków:

describe "kalkulacja ceny" do
  test "klient hurtowy dostaje cenę hurtową" do
    klient = insert(:klient, typ: :hurtowy)
    produkt = insert(:produkt, cena_detal: 100, cena_hurt: 75)

    assert Cennik.cena_dla(produkt, klient) == Decimal.new("75.00")
  end

  test "zamówienie powyżej 10 000 PLN ma darmową dostawę" do
    zamowienie = insert(:zamowienie, wartosc: Decimal.new("10001"))

    assert Zamowienie.koszt_dostawy(zamowienie) == Decimal.new("0.00")
  end

  test "zamówienie poniżej 10 000 PLN ma koszt dostawy" do
    zamowienie = insert(:zamowienie, wartosc: Decimal.new("9999"))

    assert Zamowienie.koszt_dostawy(zamowienie) == Decimal.new("25.00")
  end

  test "produkty z kategorii 'szkło' mają dodatkowy koszt pakowania" do
    produkt = insert(:produkt, kategoria: :szklo)
    zamowienie = insert(:zamowienie, pozycje: [%{produkt: produkt, ilosc: 1}])

    assert zamowienie.koszt_pakowania == Decimal.new("15.00")
  end
end

Te reguły są gdzieś zapisane - w głowie Tomka, w mailu z 2019, na karteczce na monitorze. Testy automatyczne przenoszą je do kodu. Są jednoznaczne, weryfikowalne, niemożliwe do zapomnienia. Gdy ktoś pyta "czy darmowa dostawa jest od 10 000 czy od 15 000?" - odpowiada test, nie wspomnienie Tomka.

Testy jako żywa dokumentacja

To argument, który jest niedoceniany: testy są najlepszą dokumentacją systemu.

Dokumentacja w Wordzie:

  • Dezaktualizuje się po tygodniu
  • Nikt jej nie czyta
  • Nikt nie wie, czy jest prawdziwa

Testy:

  • Jeśli system się zmieni, a test nie - test się nie powiedzie
  • Test, który przechodzi, jest gwarancją, że opisane zachowanie działa
  • Nowy programista czyta testy i wie, jak system powinien się zachowywać
# Ten test jest jednocześnie dokumentacją:
# "Faktura korygująca wymaga podania powodu korekty
#  i nie może korygować faktury starszej niż 90 dni"
test "korekta faktury starszej niż 90 dni jest odrzucana" do
  faktura = insert(:faktura, data: ~D[2025-10-01])  # ponad 90 dni temu

  assert {:error, :too_old_for_correction} =
    Faktury.koryguj(faktura, powod: "błędna ilość")
end

Nowy programista nie musi pytać "do kiedy mogę korygować fakturę?". Odpowiedź jest w teście. I ta odpowiedź jest zawsze aktualna, bo gdyby ktoś zmienił regułę na 60 dni - test by się nie powiódł i musiałby zostać zaktualizowany.

Jak to działa w praktyce - CI/CD

CI/CD (Continuous Integration / Continuous Deployment) to automatyczny pipeline, który uruchamia się przy każdej zmianie kodu:

Programista pisze kod

Pushuje do repozytorium (git)

CI automatycznie:
  1. Pobiera kod
  2. Kompiluje
  3. Uruchamia WSZYSTKIE testy (4 sekundy)
  4. Sprawdza formatowanie kodu
  5. Sprawdza bezpieczeństwo zależności

┌──── Testy przeszły? ────┐
│                          │
TAK                       NIE
│                          │
↓                          ↓
Automatyczny deploy     STOP. Kod nie wchodzi
na produkcję            na produkcję.
                        Programista dostaje
                        informację co jest nie tak.

Żaden kod nie trafia na produkcję bez przejścia testów. Nie ma "Tomek wdrożył na szybko". Nie ma "nie testowałem, bo to mała zmiana". Maszyna jest gatekeeper-em. Jest bezlitosna, bezstronna i nieomylna.

Ile kosztują testy vs ile kosztuje ich brak

Koszt pisania testów

Testy to dodatkowe 20-30% czasu developmentu. Jeśli moduł faktur trwa 4 tygodnie, testy dodadzą 1-1.5 tygodnia.

Na projekt za 200 000 PLN to dodatkowe 40 000-60 000 PLN. Duża kwota? Sprawdźmy alternatywę.

Koszt braku testów

Incydent "faktury na zero" (nasz przykład z początku):

  • 47 korekt faktur × 30 minut = 23 godziny pracy księgowej
  • Telefony do klientów z przeprosinami = 8 godzin
  • Naprawa buga + ponowne wdrożenie = 4 godziny Tomka
  • Utracone zaufanie klientów = niepoliczalne
  • Jeden incydent: ~35 godzin = 3 500-7 000 PLN

Częstotliwość incydentów bez testów: Firmy bez testów automatycznych raportują 1-4 poważne incydenty miesięcznie. Mniejsze bugi (błędne dane, zepsute raporty, problemy z uprawnieniami) - kilkanaście miesięcznie.

Roczny koszt:

  • 2 poważne incydenty/miesiąc × 5 000 PLN × 12 = 120 000 PLN
  • 10 mniejszych bugów/miesiąc × 500 PLN × 12 = 60 000 PLN
  • Razem: ~180 000 PLN rocznie

Porównanie:

  • Testy: 40 000-60 000 PLN jednorazowo + 10% czasu na utrzymanie
  • Brak testów: 180 000 PLN rocznie, rosnąco (bo system się rozrasta)

Testy zwracają się w 4-6 miesięcy. Potem każdy miesiąc to zysk.

Ukryty koszt: strach przed zmianami

Najdroższy skutek braku testów jest najtrudniejszy do zmierzenia: zespół boi się zmieniać kod.

"Nie ruszaj modułu faktur, bo coś się zepsuje." "Lepiej nie aktualizuj tej biblioteki, bo nie wiemy co się stanie." "Dodaj ten feature jako nowy moduł, nie zmieniaj istniejącego."

System staje się skostniały. Nowe funkcje buduje się obok starych, zamiast rozbudowywać istniejące. Dług techniczny rośnie. Po 3 latach masz spaghetti code, którego nikt nie chce dotykać. To jest spirala śmierci oprogramowania - i testy automatyczne ją przerywają.

Z testami: "Zmieniam moduł faktur. Testy przeszły. Wiem, że nic nie zepsułem." Zmiana jest bezpieczna. Refaktoryzacja jest bezpieczna. Rozwój jest bezpieczny.

Testy w Elixirze - dlaczego to łatwiejsze niż myślisz

Elixir i Phoenix mają wbudowane wsparcie dla testów, które jest jednym z najlepszych w branży:

ExUnit - framework testowy w standardzie

Nie trzeba nic instalować. mix test uruchamia wszystkie testy. Są częścią projektu od dnia zero.

Testy LiveView - bez przeglądarki

W React testowanie interfejsu wymaga Playwright lub Cypress - ciężkich narzędzi, które uruchamiają prawdziwą przeglądarkę. Testy są wolne (sekundy na test) i kruche (łamią się przy zmianach CSS).

W LiveView testy interfejsu działają po stronie serwera, bez przeglądarki:

test "filtrowanie zamówień po statusie", %{conn: conn} do
  insert(:zamowienie, status: :nowe)
  insert(:zamowienie, status: :wyslane)
  insert(:zamowienie, status: :nowe)

  {:ok, view, _html} = live(conn, "/zamowienia")

  # Zmień filtr
  html = view
  |> element("select[name=status]")
  |> render_change(%{status: "nowe"})

  # Sprawdź wyniki
  assert html =~ "Nowe"
  refute html =~ "Wysłane"
  assert Floki.find_all(html, "tr.zamowienie") |> length() == 2
end

Ten test sprawdza cały flow: renderowanie strony, interakcję użytkownika (zmiana filtra), odpowiedź serwera. Działa w milisekundach. Nie wymaga przeglądarki, Selenium, ani żadnej zewnętrznej infrastruktury.

Sandbox - każdy test ma czystą bazę

Ecto Sandbox daje każdemu testowi izolowaną transakcję bazodanową. Testy nie wpływają na siebie nawzajem. Mogą działać równolegle. Po teście transakcja jest cofana - baza jest czysta. Zero ręcznego sprzątania.

Szybkość

237 testów w projekcie Phoenix uruchamia się w 4-8 sekund. Nie minut - sekund. Programista uruchamia testy po każdej zmianie, bo to nic nie kosztuje. W projektach React z Cypress - pełen suite testów trwa 20-40 minut. Programista uruchamia je raz dziennie (albo wcale).

Ile testów potrzeba

Nie musisz mieć 100% pokrycia kodu testami. To akademickie i niepraktyczne. Potrzebujesz testów tam, gdzie błąd kosztuje:

Must have (niezbędne):

  • Kalkulacje finansowe (faktury, ceny, rabaty, VAT)
  • Uprawnienia (kto widzi co, kto może co zmieniać)
  • Stany magazynowe (wydanie, przyjęcie, rezerwacja)
  • Logika biznesowa z regułami (walidacja zamówień, limity kredytowe)

Should have (bardzo ważne):

  • Integracje z zewnętrznymi API (kurier, e-commerce, bank)
  • Formularze z walidacją (rejestracja, edycja klienta)
  • Raporty (czy sumują się poprawnie)

Nice to have (warto, ale nie krytyczne):

  • Wyświetlanie danych (listy, tabele, detale)
  • Nawigacja i routing
  • Style i layout

Zasada kciuka: testuj to, czego naprawianie po błędzie boli bardziej niż napisanie testu. Faktura na 0 PLN boli. Przycisk, który jest o 2 piksele za nisko - nie boli.

Na co zwrócić uwagę przy wyborze software house'u

Pytaj potencjalnego wykonawcę:

"Czy piszecie testy automatyczne?" - jeśli odpowiedź brzmi "nie, bo to spowalnia development" albo "klient nie płaci za testy", uciekaj. To firma, która oddaje Ci system bez listy kontrolnej. Pilot, który nie sprawdza paliwa przed lotem.

"Jaki macie poziom pokrycia testami?" - 70-80% dla logiki biznesowej to dobry wynik. 0% to czerwona flaga. 100% to prawdopodobnie kłamstwo.

"Czy macie CI/CD?" - każdy push do repozytorium powinien uruchamiać testy automatycznie. Brak CI/CD oznacza, że testy (jeśli istnieją) nie są uruchamiane regularnie, więc mogą być nieaktualne.

"Czy mogę zobaczyć wynik testów?" - dobra firma pokaże Ci zielony pipeline w CI. Każdy build, każdy test, każdy wynik. Transparentność.

Podsumowanie

Testy automatyczne to nie fanaberia programistów. To ubezpieczenie Twojego biznesu od błędów, które kosztują pieniądze, czas i reputację.

AspektBez testówZ testami
Wdrożenie zmiany"Tomek przetestował ręcznie"237 scenariuszy sprawdzonych w 4 sekundy
Incydenty na produkcji2-4 poważne / miesiącBliskie zeru
Czas naprawy bugaGodziny (szukanie co się zepsuło)Minuty (test mówi co nie działa)
Nowy programistaBoi się ruszać kodZmienia z pewnością (testy pilnują)
Refaktoryzacja"Nie ruszaj, bo się zepsuje"Bezpieczna (testy złapią regresję)
DokumentacjaNieaktualna lub brakTesty = żywa dokumentacja
Koszt roczny~180 000 PLN na naprawę incydentów~50 000 PLN na pisanie testów

Budujesz nowy system i chcesz mieć pewność, że będzie działał poprawnie? Porozmawiajmy - testy automatyczne są standardową częścią każdego naszego projektu. Nie jako dodatek - jako fundament.