Wprowadzenie do testowania

Opublikowany: 2021-06-16

Witamy w naszej nowej serii blogów o wszystkim, co związane jest z testowaniem . Mamy nadzieję, że te posty na blogu dadzą ci ogólne pojęcie o tym, jak piszemy je tutaj w Mediatoolkit i dlaczego. Zanim zagłębimy się w przykłady, zacznijmy od kilku podstawowych definicji, pomysłów i korzyści płynących z pisania testów.

Jakie testy są dostępne?

Nie wszystkie testy są sobie równe. Istnieją różne rodzaje testów do różnych celów. Chociaż ten post koncentruje się głównie na testach jednostkowych , powinniśmy być świadomi różnic i rozumieć korzyści z każdego z nich.

1. Testy ręczne

Testowanie ręczne obejmuje testy wykonywane ręcznie przez testera. Zajmują się nimi zwykle dział kontroli jakości lub sami programiści . Kontrola jakości powinna być postrzegana jako ostatnia linia obrony, a nie główny tester aplikacji (kontrola jakości wykonująca testy ręczne jako główny sposób testowania interfejsu użytkownika jest ważnym wyjątkiem w tym przypadku).

Deweloperzy zazwyczaj piszą testy manualne, gdy chcą uruchomić pewne rzeczy lokalnie i zobaczyć, jak zachowuje się system bez wciskania kodu do fazy stagingu lub, Boże chroń, produkcji. Jednak celem tych testów nie jest odporność kodu. Jedyną rolą tych testów jest wyłapywanie błędów i zapewnienie jakości produktu .

2. Testy integracyjne

Systemy składają się z wielu elementów, a przynajmniej tak powinny być. Testy integracyjne sprawdzają publiczne API tych komponentów. Nie tylko REST API, ale każdy publicznie dostępny interfejs API, taki jak komunikacja z tematem Kafka.

Jeśli komponent „mówi”, wyświetla komunikat na dany temat w określonym formacie, to znaczy jego publicznym interfejsie API lub kontrakcie. Testowanie integracyjne polega na sprawdzeniu, czy wiele indywidualnie opracowanych komponentów może się komunikować i czy ich umowy są zgodne jako grupa.

Jeśli przetestujesz te komponenty tylko pojedynczo, ich indywidualne zachowanie może działać poprawnie, ale gdy spróbujesz połączyć je w większą grupę, odkryjesz, że ich kontrakty się różnią .

3. Testy od końca do końca (E2E)

Testy typu end-to-end zapewniają przepływ doświadczeń użytkownika w produkcie końcowym . Złożoność dzisiejszych systemów jest trudna do pokrycia testami. Wiele systemów polega na sobie nawzajem, a testy E2E zapewniają, że produkt spełnia oczekiwania . Kontrola jakości weryfikuje poprawność produktu, przechodząc przez przepływ użytkowników końcowych i sprawdzając, czy wszystkie systemy zachowują się zgodnie z oczekiwaniami.

4. Testy jednostkowe

Testy jednostkowe są podstawą każdego niezawodnego oprogramowania . Stanowią podstawę do innych testów. Testują poszczególne jednostki.

Deweloperzy mogą pomylić definicję jednostki z jedną metodą. Testy jednostkowe powinny testować zachowanie składnika, który może składać się z wielu różnych klas. Należy przetestować metody publiczne dostępne z innych komponentów, nie należy testować chronionych metod lub klas. Są to szczegóły implementacji, a nie część Twojego publicznego interfejsu API .

Dlaczego mielibyśmy w ogóle testować?

Jeśli piszemy dobre testy i piszemy je często, możemy zapewnić jakość naszego produktu, zanim ujrzy światło dzienne.

W miarę starzenia się naszego systemu korzyści płynące z testowania stają się coraz bardziej widoczne. Nasze środowiska stają się niezawodne, oszczędzają czas rozwoju i dużo wyrywania włosów, gdy nieuchronnie coś pójdzie nie tak. Twoi koledzy będą wdzięczni, gdy będą mogli rzucić okiem na twoje testy i zrozumieć, co twój kod powinien, a czego nie powinien robić bez konieczności ręcznego uruchamiania .

Solidny kod

Zanim zaczniemy mówić o „solidnym kodzie”, jak go zdefiniować? Co sprawia, że ​​kod jest niezawodny? Czy to oznacza, że ​​powinniśmy programować defensywnie i zastanowić się, jak inni programiści mogą nadużywać naszego kodu?

Solidny kod jest zawsze prosty i przejrzysty. Prostota jest solidna, złożoność jest krucha . Powinniśmy radzić sobie z nieprawidłowymi danymi wejściowymi, ale to nie znaczy, że musimy programować defensywnie i nie ufać naszemu zespołowi.

Prawo Galla : niezmiennie okazuje się, że złożony system, który działa, wyewoluował z prostego systemu, który działał.

Odwrotna propozycja również wydaje się prawdziwa: złożony system zaprojektowany od podstaw nigdy nie działa i nie można go uruchomić. Musisz zacząć od nowa, zaczynając od działającego prostego systemu.

Bezpieczna refaktoryzacja

Gdy Twój kod jest objęty testami, nie obawiasz się już zmiany istniejącego kodu . Po każdej zmianie możesz uruchomić testy i upewnić się, że nie zepsułeś rzeczy. Kiedy masz testy, nie musisz programować defensywnie.

Refaktoryzacja bez testów to zejście ze śliskiego zbocza, które skończy się nieprzespanymi nocami i niedzielami w pracy. Ten temat jest zbyt obszerny, aby można go było tutaj omówić, i zasługuje na osobny wpis na blogu w przyszłości.

Jak nie piszemy testów?

Równie ważne jest, aby wiedzieć, jak NIE pisać testów jednostkowych , jak je pisać.

  • Pisanie testu, który drukuje wynik wywołania metody, nie jest testem, ponieważ nie sprawdzamy pożądanego wyniku .
  • Jeśli test odczytuje dane z pliku w folderze Dokumenty, nie jest to prawdziwy test jednostkowy, ponieważ testy nie powinny zależeć od środowiska.
  • Każdy programista powinien być w stanie sprawdzić Twój kod i pomyślnie przeprowadzić testy bez robienia czegokolwiek innego.
  • Każdy test jednostkowy powinien być niezależny od innych testów . Oznacza to, że kolejność wykonywania testów również nie powinna mieć znaczenia.
  • Wielokrotne uruchamianie testów powinno zawsze kończyć się tymi samymi wynikami , jeśli nie zmieniamy żadnego kodu.
  • Testy jednostkowe powinny testować zachowania , a nie poszczególne wywołania metod. Nie każda klasa i metoda musi mieć swój test.

Zachowanie to coś, co zapewnia rzeczywistą wartość, której potrzebuje użytkownik twojego systemu. Czy użytkownik musi wiedzieć, czy productFactory.create() utworzył ten sam obiekt przy dwukrotnym wywołaniu, czy też repozytorium zostało wywołane z pewnymi parametrami? Prawdopodobnie nie, ale wciąż wielu programistów pisze właśnie takie testy.

Jeśli twoje testy wyglądają tak, są ściśle powiązane z twoją implementacją. Za każdym razem, gdy chcesz zmienić szczegóły swojej implementacji, musisz zaktualizować swoje testy, nawet jeśli zachowanie jest takie samo. Twoje testy powinny się zmieniać tylko wtedy, gdy zmieni się zachowanie, a nie szczegóły implementacji . Innymi słowy, przetestuj to, co robi Twój kod, a nie jak to robi.

Jak piszemy testy?

Nasze testy muszą być zgodne z najlepszymi praktykami kodu , muszą być niezależne od środowiska i muszą być wykonywane szybko .

Ważne jest, aby czas wykonania testu był jak najkrótszy. Każdy test nie powinien zająć więcej niż kilka milisekund . Gdy wykonanie testów trwa zbyt długo, ludzie zwykle je pomijają i po prostu polegają na swoim serwerze CI, takim jak Jenkins, który robi zamieszanie, gdy nie może zbudować plików wykonywalnych wdrażania.

Każdy test składa się z 3 sekcji „A” (wzorzec AAA) :

  1. Zorganizować
  2. działać
  3. Zapewniać

1. Rozmieść

W sekcji aranżacji naszego testu upewniamy się, że nasz system jest w określonym stanie przed wywołaniem zachowania, które chcemy przetestować . „System” może być obiektem, który musimy skonfigurować w określony sposób, aby wywołać zachowanie, tworząc tymczasowe pliki lub rzeczy tego rodzaju.

Kod w tej sekcji jest zwykle większy niż pozostałe dwa razem wzięte .

Jednym z wzorców projektowych, który powinien okazać się szczególnie przydatny, aby ta sekcja była mała, jest Object Mother . Ten wzorzec projektowy jest bardzo podobny do Factory , ale zawiera bardziej szczegółowe metody tworzenia wstępnie skonfigurowanych obiektów. Podczas gdy standardowa Factory może mieć metodę taką jak createCar(carDescription) , ObjectMother będzie mieć metody takie jak createRedFerrari() , createBlackTesla() lub createBrokenYugo() .

2. Działaj

Ta sekcja twojego testu musi mieć jedną linię . Ten wiersz wykonuje testowane zachowanie. Jeśli zauważysz, że piszesz więcej niż jedną linijkę w tej sekcji, prawdopodobnie nie masz odpowiedniego opisu swojego zachowania . Nie powinno się oczekiwać, że Twoi klienci będą wywoływać wiele metod Twojego obiektu w określonej kolejności, więc po co Twoje testy?

Ten wiersz to wywołanie metody, którą chcemy przetestować. Jeśli ta metoda zwraca wynik, należy przechowywać tę wartość w zmiennej, aby sprawdzić, czy jest to oczekiwana wartość w kroku Assert.

3. Potwierdź

Po przygotowaniu systemu w sekcji Rozmieść i wykonaniu testowanej akcji w sekcji Działaj, musimy zweryfikować wynik akcji. Zwykle sprawdzamy tutaj wynik metody, ale czasami nasze metody nie zwracają wartości, ale nadal powodują skutki uboczne. Jeśli oczekiwano, że nasz kod zmieni stan obiektu, utworzy plik lub usunie coś z List , powinniśmy sprawdzić, czy dokładnie to zrobił.

Stubs vs Mocks

Większość programistów używa terminów mock i stub zamiennie, ale istnieją różnice.

Odcinek nie może oblać testu, próba może.

Odcinki są oparte na stanie , zwracają wartości zakodowane na stałe. „Jaki wynik uzyskałem?”

Mocki są oparte na zachowaniu , używasz ich do weryfikacji, w jaki sposób przechodzi przez nie Twoje zachowanie. „Jak uzyskałem wynik?”

Jeśli twoje testy jednostkowe są zaśmiecone mockami, testy kończą się bardzo kruche, co oznacza, że ​​za każdym razem, gdy zmieniasz jeden ze szczegółów implementacji, musisz zaktualizować wszystkie swoje symulacje.

Przetestuj tylko jedną rzecz.

Musimy być w stanie wyodrębnić jedno zachowanie i udowodnić, że działa . Jeśli to zachowanie powinno działać inaczej z różnymi danymi wejściowymi, musimy napisać nowy test dla każdego z tych zachowań. Trudno powiedzieć, dlaczego nasz test się nie powiódł, jeśli mamy duży test, który testuje wiele rzeczy naraz. Co więcej, coraz trudniej jest usunąć funkcje, których już nie potrzebujemy, i zobaczyć, które funkcje zepsuliśmy, dodając nowy kod.

W końcowej części testów dobrze jest mieć wiele asercji, o ile nie wymaga to wielokrotnego wywoływania testowanego zachowania. W takim przypadku trudno będzie wskazać wadliwe zachowanie i je naprawić.

Kiedy potwierdzamy wiele zachowań w jednym teście, nie mamy jasnego obrazu tego, co dokładnie nie działa, ponieważ test zgłosi tylko pierwszy błąd, a pozostałe zostaną pominięte. Dużo trudniej jest zrozumieć, jakie zmiany są konieczne i ile rzeczy nie działa zgodnie z oczekiwaniami.

Testy nazewnictwa

Jedną rzeczą, która oddziela dobre testy od świetnych testów, jest nazwa testu. Testy powinny nam nie tylko mówić, co robią, ale kiedy to robią.

Istnieje wiele dobrych wzorców nazewnictwa, których możesz użyć, więc wybierz ten, który uważasz za najbardziej opisowy i trzymaj się go. Oto kilka przykładów świetnych nazw testów:

  • RejestracjaServiceShould.createNewAccountGdyE-mailNieZdjęty
  • RegistrationServiceTest.whenEmailIsFree_createNewAccount
  • RegistrationServiceTest.if_freeEmail_when_userCreatesAccount_then_create

Podczas pisania testów jednostkowych o wiele ważniejsze jest komunikowanie tego, co testujesz, niż przestrzeganie najlepszych praktyk nazewnictwa metod. Na przykład w Javie używamy camelCase podczas pisania metod, ale doskonale jest używać podkreślenia (_) do oddzielenia stanu od akcji w nazwie testu.

Czyste testy

Testy, które piszesz, powinny być zgodne ze wszystkimi praktykami czystego kodu, które stosujesz w swoim kodzie. Testy nie są obywatelami drugiej kategorii i musisz zachować ten sam poziom staranności, co resztę kodu, aby były czytelne.

Bardzo ważna jest definicja duplikacji kodu w testach. Zasada DRY (Don't Repeat Yourself) odnosi się do wydobywania zachowań, które zmieniają się z tego samego powodu. Testy zmieniają się z różnych powodów, więc zróżnicuj wyodrębnianie elementów z testów, jeśli naprawdę nie zmieniają się z tego samego powodu. Uwaga, spoiler, często nie.

if stwierdzenia nie należą do testów. Stwierdzenie if mówi nam, że nasz test robi co najmniej dwie różne rzeczy i byłoby lepiej, gdybyśmy przepisali nasz test jako dwa różne testy. Poprawnie nazwane, łatwiej będzie zrozumieć, co robią testy i jakie są wszystkie różne zachowania.

Kiedy powinniśmy pisać testy?

Kierując się zasadami TDD, powinniśmy pisać testy przed napisaniem nowego kodu .

Kiedy musimy dodać nową funkcję, najpierw opisujemy pożądane zachowanie jako nowy test. Wprowadzamy najmniej zmian niezbędnych do przejścia tego testu bez naruszania innych.

W przeciwnym razie, im więcej czasu mija, tym więcej mamy kodu, który nie został przetestowany, i rośnie szansa na wprowadzenie błędów lub przeróbek .

Ponadto testy stają się bardziej złożone, ponieważ mamy teraz więcej kodu do przetestowania, a zazwyczaj programiści dostosowują testy do kodu. Oznacza to, że dostosowujemy zachowanie, aby pasowało do tego, co robi nasz kod, a nie na odwrót.

Kiedy piszemy testy wcześnie, definicja problemu staje się mniejsza i łatwiej jest owinąć głowę wokół takich zagadnień niż bardziej ogólne i złożone zachowania.

Końcowe przemyślenia

Chociaż pisanie testów może wydawać się czynnością opcjonalną, ważne jest, aby zacząć od dobrych podstaw. Kodowanie jest już trudne, ułatw sobie i swoim kolegom z zespołu, pisząc testowalny kod, który jest łatwiejszy do odczytania, zrozumienia i utrzymania . Wreszcie, jeśli masz trudności z dostosowaniem testów do swoich życzeń, problem jest bardziej prawdopodobny w twoim kodzie niż w testach.

Chcesz pracować nad czystym i testowalnym kodem?
Sprawdź naszą otwartą pozycję dla Senior Backend Developer
lub wyślij nam otwartą aplikację !