8 najlepszych praktyk w zakresie testowania zautomatyzowanego dla pozytywnego doświadczenia w testowaniu
Opublikowany: 2022-03-11Nic dziwnego, że wielu programistów postrzega testowanie jako zło konieczne, które zabiera czas i energię: testowanie może być żmudne, bezproduktywne i całkowicie zbyt skomplikowane.
Moje pierwsze doświadczenie z testowaniem było okropne. Pracowałem w zespole, który miał rygorystyczne wymagania dotyczące pokrycia kodu. Przepływ pracy polegał na: zaimplementowaniu funkcji, debugowaniu jej i pisaniu testów, aby zapewnić pełne pokrycie kodu. Zespół nie miał testów integracyjnych, tylko testy jednostkowe z mnóstwem ręcznie zainicjowanych mocków, a większość testów jednostkowych testowała trywialne ręczne mapowania przy użyciu biblioteki do wykonywania automatycznych mapowań. Każdy test próbował potwierdzić każdą dostępną właściwość, więc każda zmiana niszczyła dziesiątki testów.
Nie lubiłem pracować z testami, ponieważ postrzegano je jako czasochłonne obciążenie. Jednak nie poddałem się. Testy pewności i automatyzacja kontroli po każdej drobnej zmianie wzbudziły moje zainteresowanie. Zacząłem czytać i ćwiczyć, i dowiedziałem się, że testy, jeśli zostaną wykonane prawidłowo, mogą być zarówno pomocne, jak i przyjemne.
W tym artykule podzielę się ośmioma najlepszymi praktykami dotyczącymi automatycznego testowania, których nie znałem od samego początku.
Dlaczego potrzebujesz zautomatyzowanej strategii testowania
Testowanie automatyczne często koncentruje się na przyszłości, ale jeśli je poprawnie zaimplementujesz, od razu odniesiesz korzyści. Korzystanie z narzędzi, które pomogą Ci lepiej wykonywać swoją pracę, może zaoszczędzić czas i sprawić, że praca będzie przyjemniejsza.
Wyobraź sobie, że tworzysz system, który pobiera zamówienia zakupu z systemu ERP firmy i umieszcza je u dostawcy. Masz w ERP cenę wcześniej zamówionych pozycji, ale aktualne ceny mogą być inne. Chcesz kontrolować, czy złożyć zamówienie po niższej czy wyższej cenie. Masz zapisane preferencje użytkownika i piszesz kod do obsługi wahań cen.
Jak sprawdzisz, czy kod działa zgodnie z oczekiwaniami? Prawdopodobnie:
- Utwórz fikcyjną kolejność w wystąpieniu programisty ERP (zakładając, że wcześniej je skonfigurowałeś).
- Uruchom swoją aplikację.
- Wybierz to zamówienie i rozpocznij proces składania zamówienia.
- Zbierz dane z bazy ERP.
- Poproś o aktualne ceny z interfejsu API dostawcy.
- Zastąp ceny w kodzie, aby stworzyć określone warunki.
Zatrzymałeś się w punkcie przerwania i możesz przejść krok po kroku, aby zobaczyć, co stanie się dla jednego scenariusza, ale jest wiele możliwych scenariuszy:
| Preferencje | Cena ERP | Cena dostawcy | Czy powinniśmy złożyć zamówienie? | |
|---|---|---|---|---|
| Zezwól na wyższą cenę | Zezwól na niższą cenę | |||
| fałszywe | fałszywe | 10 | 10 | prawda |
| (Tu byłyby jeszcze trzy kombinacje preferencji, ale ceny są równe, więc wynik jest taki sam.) | ||||
| prawda | fałszywe | 10 | 11 | prawda |
| prawda | fałszywe | 10 | 9 | fałszywe |
| fałszywe | prawda | 10 | 11 | fałszywe |
| fałszywe | prawda | 10 | 9 | prawda |
| prawda | prawda | 10 | 11 | prawda |
| prawda | prawda | 10 | 9 | prawda |
W przypadku błędu firma może stracić pieniądze, zaszkodzić reputacji lub jedno i drugie. Musisz sprawdzić wiele scenariuszy i powtórzyć pętlę sprawdzania kilka razy. Robienie tego ręcznie byłoby nużące. Ale testy są tutaj, aby pomóc!
Testy umożliwiają tworzenie dowolnego kontekstu bez wywołań niestabilnych interfejsów API. Eliminują potrzebę wielokrotnego klikania przez stare i powolne interfejsy, które są zbyt powszechne w starszych systemach ERP. Wszystko, co musisz zrobić, to zdefiniować kontekst dla jednostki lub podsystemu, a następnie wszelkie debugowanie, rozwiązywanie problemów lub eksploracja scenariusza odbywają się natychmiast — uruchamiasz test i wracasz do swojego kodu. Preferuję ustawienie skrótu klawiszowego w moim środowisku IDE, które powtarza mój poprzedni test, dając natychmiastową, zautomatyzowaną informację zwrotną, gdy wprowadzam zmiany.
1. Utrzymuj właściwe nastawienie
W porównaniu z ręcznym debugowaniem i samotestowaniem, testy automatyczne są bardziej produktywne od samego początku, nawet przed wprowadzeniem kodu testowego. Po sprawdzeniu, czy Twój kod zachowuje się zgodnie z oczekiwaniami — przez ręczne testowanie lub, w przypadku bardziej złożonego modułu, poprzez przechodzenie przez niego za pomocą debugera podczas testowania — możesz użyć asercji, aby zdefiniować, czego oczekujesz dla dowolnej kombinacji parametrów wejściowych.
Po przejściu testów jesteś prawie gotowy do zatwierdzenia, ale nie do końca. Przygotuj się do refaktoryzacji kodu, ponieważ pierwsza działająca wersja zwykle nie jest elegancka. Czy wykonałbyś tę refaktoryzację bez testów? To wątpliwe, ponieważ musiałbyś ponownie wykonać wszystkie ręczne czynności, co mogłoby zmniejszyć twój entuzjazm.
Co z przyszłością? Podczas przeprowadzania wszelkich refaktoryzacji, optymalizacji lub dodawania funkcji testy pomagają upewnić się, że moduł nadal zachowuje się zgodnie z oczekiwaniami po jego zmianie, co daje trwałą pewność siebie i pozwala programistom czuć się lepiej przygotowanymi do radzenia sobie z nadchodzącymi pracami.
Myślenie o testach jako o obciążeniu lub czymś, co uszczęśliwia tylko recenzentów kodu lub liderów, przynosi efekt przeciwny do zamierzonego. Testy to narzędzie, z którego my jako programiści korzystamy. Lubimy, gdy nasz kod działa i nie lubimy spędzać czasu na powtarzających się czynnościach lub naprawianiu kodu w celu usunięcia błędów.
Ostatnio pracowałem nad refaktoryzacją w mojej bazie kodu i poprosiłem moje IDE o wyczyszczenie using dyrektyw. Ku mojemu zdziwieniu testy wykazały kilka błędów w moim systemie raportowania poczty e-mail. Jednak był to prawidłowy błąd — proces czyszczenia usunął niektóre dyrektywy using w moim kodzie Razor (HTML + C#) dla szablonu wiadomości e-mail, w wyniku czego aparat szablonów nie był w stanie zbudować prawidłowego kodu HTML. Nie spodziewałem się, że tak drobna operacja zepsuje raportowanie e-mailowe. Testowanie pomogło mi uniknąć godzin spędzonych na łapaniu błędów w aplikacji tuż przed jej wydaniem, kiedy zakładałem, że wszystko będzie działać.
Oczywiście trzeba umieć posługiwać się narzędziami, a nie skaleczyć przysłowiowych palców. Mogłoby się wydawać, że definiowanie kontekstu jest żmudne i może być trudniejsze niż uruchamianie aplikacji, że testy wymagają zbyt wiele konserwacji, aby nie stały się przestarzałe i bezużyteczne. To są ważne punkty i zajmiemy się nimi.
2. Wybierz odpowiedni typ testu
Deweloperzy często nie lubią testów automatycznych, ponieważ próbują wyśmiewać kilkanaście zależności tylko po to, by sprawdzić, czy są one wywoływane przez kod. Alternatywnie, programiści napotykają test wysokiego poziomu i próbują odtworzyć każdy stan aplikacji, aby sprawdzić wszystkie odmiany w małym module. Te wzorce są nieproduktywne i nużące, ale możemy ich uniknąć, wykorzystując różne typy testów zgodnie z ich przeznaczeniem. (Testy powinny być w końcu praktyczne i przyjemne!)
Czytelnicy będą musieli wiedzieć, czym są testy jednostkowe i jak je pisać, a także znać testy integracyjne — jeśli nie, warto zatrzymać się tutaj, aby przyspieszyć.
Istnieją dziesiątki typów testów, ale te pięć powszechnych typów tworzy niezwykle skuteczną kombinację:
- Testy jednostkowe służą do testowania wyizolowanego modułu przez bezpośrednie wywołanie jego metod. Zależności nie są testowane, dlatego są wyszydzane.
- Testy integracyjne służą do testowania podsystemów. Nadal używasz bezpośrednich wywołań własnych metod modułu, ale tutaj zależy nam na zależnościach, więc nie używaj mockowanych zależności — tylko rzeczywiste (produkcyjne) moduły zależne. Nadal możesz używać bazy danych w pamięci lub fałszywego serwera WWW, ponieważ są to pozory infrastruktury.
- Testy funkcjonalne to testy dla całej aplikacji, zwane również testami end-to-end (E2E). Nie korzystasz z bezpośrednich połączeń. Zamiast tego cała interakcja przechodzi przez interfejs API lub interfejs użytkownika — są to testy z perspektywy użytkownika końcowego. Jednak infrastruktura nadal jest wyśmiewana.
- Testy Canary są podobne do testów funkcjonalnych, ale z infrastrukturą produkcyjną i mniejszym zestawem działań. Służą do zapewnienia działania nowo wdrożonych aplikacji.
- Testy obciążeniowe są podobne do testów kanarkowych, ale z rzeczywistą infrastrukturą pomostową i jeszcze mniejszym zestawem czynności, które powtarzają się wielokrotnie.
Nie zawsze trzeba od początku pracować ze wszystkimi pięcioma typami testów. W większości przypadków z pierwszymi trzema testami możesz przejść długą drogę.
Pokrótce przeanalizujemy przypadki użycia każdego typu, aby pomóc Ci wybrać te odpowiednie dla Twoich potrzeb.
Testy jednostkowe
Przypomnij sobie przykład z różnymi cenami i preferencjami obsługi. To dobry kandydat do testów jednostkowych, ponieważ zależy nam tylko na tym, co dzieje się wewnątrz modułu, a wyniki mają istotne konsekwencje biznesowe.
Moduł ma wiele różnych kombinacji parametrów wejściowych i chcemy uzyskać prawidłową wartość zwracaną dla każdej kombinacji prawidłowych argumentów. Testy jednostkowe są dobre w zapewnianiu poprawności, ponieważ zapewniają bezpośredni dostęp do parametrów wejściowych funkcji lub metody i nie musisz pisać dziesiątek metod testowych, aby objąć każdą kombinację. W wielu językach można uniknąć duplikowania metod testowych, definiując metodę, która akceptuje argumenty potrzebne w kodzie i oczekiwane wyniki. Następnie możesz użyć swoich narzędzi testowych, aby zapewnić różne zestawy wartości i oczekiwań dla tej sparametryzowanej metody.
Testy integracyjne
Testy integracyjne dobrze sprawdzają się w przypadkach, gdy interesuje Cię, jak moduł współdziała z jego zależnościami, innymi modułami lub infrastrukturą. Nadal używasz bezpośrednich wywołań metod, ale nie masz dostępu do podmodułów, więc próba przetestowania wszystkich scenariuszy dla wszystkich metod wejściowych wszystkich podmodułów jest niepraktyczna.
Zazwyczaj wolę mieć jeden scenariusz sukcesu i jeden scenariusz niepowodzenia na moduł.
Lubię używać testów integracyjnych, aby sprawdzić, czy kontener wstrzykiwania zależności został pomyślnie zbudowany, czy potok przetwarzania lub obliczeń zwraca oczekiwany wynik lub czy złożone dane zostały poprawnie odczytane i przekonwertowane z bazy danych lub API innej firmy.
Testy funkcjonalne lub E2E
Testy te dają największą pewność, że Twoja aplikacja działa, ponieważ sprawdzają, czy Twoja aplikacja może przynajmniej uruchomić się bez błędu w czasie wykonywania. Rozpoczęcie testowania kodu bez bezpośredniego dostępu do jego klas wymaga trochę więcej pracy, ale kiedy zrozumiesz i napiszesz kilka pierwszych testów, przekonasz się, że nie jest to zbyt trudne.
Uruchom aplikację, uruchamiając proces z argumentami wiersza polecenia, jeśli to konieczne, a następnie użyj aplikacji tak, jak zrobiłby to potencjalny klient: wywołując punkty końcowe interfejsu API lub naciskając przyciski. Nie jest to trudne, nawet w przypadku testowania interfejsu użytkownika: każda główna platforma ma narzędzie do wyszukiwania elementu wizualnego w interfejsie użytkownika.
Testy kanaryjskie
Testy funkcjonalne informują, czy Twoja aplikacja działa w środowisku testowym, ale co ze środowiskiem produkcyjnym? Załóżmy, że pracujesz z kilkoma interfejsami API innych firm i chcesz mieć pulpit nawigacyjny ich stanów lub chcesz zobaczyć, jak Twoja aplikacja obsługuje przychodzące żądania. Są to typowe przypadki użycia testów kanarków.
Działają przez krótkie działanie na działający system, nie powodując skutków ubocznych dla systemów innych firm. Na przykład możesz zarejestrować nowego użytkownika lub sprawdzić dostępność produktu bez składania zamówienia.
Celem testów canary jest upewnienie się, że wszystkie główne komponenty współpracują ze sobą w środowisku produkcyjnym, nie zawodząc na przykład z powodu problemów z poświadczeniami.
Testy obciążenia
Testy obciążeniowe pokazują, czy Twoja aplikacja będzie nadal działać, gdy zacznie z niej korzystać duża liczba osób. Są podobne do testów kanarkowych i funkcjonalnych, ale nie są przeprowadzane w środowiskach lokalnych ani produkcyjnych. Zwykle używane jest specjalne środowisko pomostowe, które jest podobne do środowiska produkcyjnego.
Należy zauważyć, że te testy nie korzystają z prawdziwych usług innych firm, które mogą być niezadowolone z zewnętrznych testów obciążenia swoich usług produkcyjnych i mogą w rezultacie pobierać dodatkowe opłaty.
3. Zachowaj oddzielne typy testów
Podczas opracowywania planu testów automatycznych każdy rodzaj testu powinien być rozdzielony, tak aby mógł działać niezależnie. Chociaż wymaga to dodatkowej organizacji, warto, ponieważ testy mieszania mogą powodować problemy.
Te testy mają różne:
- Intencje i podstawowe koncepcje (więc oddzielenie ich stanowi dobry precedens dla następnej osoby patrzącej na kod, w tym „przyszłość ty”).
- Czasy wykonania (więc najpierw uruchomienie testów jednostkowych pozwala na szybszy cykl testów, gdy test się nie powiedzie).
- Zależności (więc wydajniejsze jest ładowanie tylko tych potrzebnych w typie testowania).
- Wymagana infrastruktura.
- Języki programowania (w niektórych przypadkach).
- Stanowiska w potoku ciągłej integracji (CI) lub poza nim.
Należy zauważyć, że w przypadku większości języków i stosów technologii można na przykład grupować wszystkie testy jednostkowe wraz z podfolderami nazwanymi od modułów funkcjonalnych. Jest to wygodne, zmniejsza tarcie podczas tworzenia nowych modułów funkcjonalnych, jest łatwiejsze do zautomatyzowanych kompilacji, skutkuje mniejszym bałaganem i jest kolejnym sposobem na uproszczenie testowania.
4. Uruchom swoje testy automatycznie
Wyobraź sobie sytuację, w której napisałeś kilka testów, ale po wyciągnięciu repozytorium kilka tygodni później zauważasz, że te testy już nie przechodzą.
Jest to nieprzyjemne przypomnienie, że testy to kod i jak każdy inny fragment kodu, trzeba je pielęgnować. Najlepszy czas na to jest tuż przed momentem, w którym myślisz, że zakończyłeś pracę i chcesz sprawdzić, czy wszystko nadal działa zgodnie z przeznaczeniem. Masz cały potrzebny kontekst i możesz łatwiej naprawić kod lub zmienić nieudane testy niż twój kolega pracujący na innym podsystemie. Ale ten moment istnieje tylko w Twojej głowie, więc najczęstszym sposobem na uruchomienie testów jest automatyczne po wypchnięciu do gałęzi deweloperskiej lub po utworzeniu pull requesta.

W ten sposób twoja główna gałąź zawsze będzie w prawidłowym stanie, a przynajmniej będziesz miał jasne wskazanie jej stanu. Zautomatyzowany potok budowania i testowania — lub potok CI — pomaga:
- Upewnij się, że kod jest możliwy do zbudowania.
- Wyeliminuj potencjalne problemy „Działa na moim komputerze” .
- Zapewnij uruchamialne instrukcje dotyczące przygotowania środowiska programistycznego.
Konfigurowanie tego potoku zajmuje trochę czasu, ale potok może ujawnić szereg problemów, zanim dotrą do użytkowników lub klientów, nawet jeśli jesteś jedynym deweloperem.
Po uruchomieniu CI ujawnia również nowe problemy, zanim będą miały szansę poszerzyć zakres. W związku z tym wolę to ustawić zaraz po napisaniu pierwszego testu. Możesz hostować swój kod w prywatnym repozytorium na GitHub i skonfigurować akcje GitHub. Jeśli Twoje repozytorium jest publiczne, masz jeszcze więcej opcji niż Akcje GitHub. Na przykład mój plan testów automatycznych działa na AppVeyor, dla projektu z bazą danych i trzema rodzajami testów.
Wolę układać swój potok dla projektów produkcyjnych w następujący sposób:
- Kompilacja lub transpilacja
- Testy jednostkowe: są szybkie i nie wymagają zależności
- Konfiguracja i inicjalizacja bazy danych lub innych usług
- Testy integracyjne: mają zależności poza twoim kodem, ale są szybsze niż testy funkcjonalne
- Testy funkcjonalne: po pomyślnym zakończeniu innych kroków uruchom całą aplikację
Nie ma testów kanarkowych ani testów obciążeniowych. Ze względu na swoją specyfikę i wymagania należy je inicjować ręcznie.
5. Napisz tylko niezbędne testy
Pisanie testów jednostkowych dla całego kodu jest powszechną strategią, ale czasami jest to strata czasu i energii oraz nie daje pewności. Jeśli znasz koncepcję „piramidy testowej”, możesz pomyśleć, że cały Twój kod musi być objęty testami jednostkowymi, a tylko podzbiór objęty innymi testami wyższego poziomu.
Nie widzę potrzeby pisania testu jednostkowego, który zapewnia, że kilka symulowanych zależności zostanie wywołanych w żądanej kolejności. Wykonanie tego wymaga skonfigurowania kilku makiet i zweryfikowania wszystkich połączeń, ale nadal nie dałoby mi pewności, że moduł działa. Zwykle piszę tylko test integracyjny, który wykorzystuje rzeczywiste zależności i sprawdza tylko wynik; to daje mi pewność, że potok w testowanym module działa poprawnie.
Generalnie piszę testy, które ułatwiają mi życie przy wdrażaniu funkcjonalności i wspieraniu jej później.
W przypadku większości aplikacji dążenie do 100% pokrycia kodu oznacza dużo żmudnej pracy i eliminuje radość z pracy z testami i ogólnie z programowaniem. Jak ujął to Martin Fowler's Test Coverage:
Pokrycie testowe to przydatne narzędzie do znajdowania nieprzetestowanych części bazy kodu. Pokrycie testów jest mało przydatne jako liczbowe określenie, jak dobre są twoje testy.
Dlatego polecam zainstalować i uruchomić analizator pokrycia po napisaniu kilku testów. Raport z wyróżnionymi liniami kodu pomoże Ci lepiej zrozumieć jego ścieżki wykonania i znaleźć odkryte miejsca, które należy uwzględnić. Ponadto, patrząc na swoje gettery, setery i fasady, zobaczysz, dlaczego 100% pokrycia nie jest zabawne.
6. Zagraj w Lego
Od czasu do czasu pojawiają się pytania typu „Jak mogę przetestować metody prywatne?” Ty nie. Jeśli zadałeś to pytanie, coś już poszło nie tak. Zwykle oznacza to, że naruszyłeś zasadę pojedynczej odpowiedzialności, a Twój moduł nie robi czegoś właściwie.
Refaktoryzuj ten moduł i przenieś logikę, którą uważasz za ważną, do osobnego modułu. Nie ma problemu ze zwiększeniem liczby plików, co doprowadzi do powstania kodu o strukturze klocków Lego: bardzo czytelnego, łatwego w utrzymaniu, zastępowalnego i testowalnego.
Łatwiej powiedzieć o właściwej strukturze kodu niż zrobić. Oto dwie sugestie:
Programowanie funkcjonalne
Warto poznać zasady i idee programowania funkcjonalnego. Większość popularnych języków, takich jak C, C++, C#, Java, Assembly, JavaScript i Python, zmusza do pisania programów dla maszyn. Programowanie funkcjonalne jest lepiej dostosowane do ludzkiego mózgu.
Na pierwszy rzut oka może się to wydawać sprzeczne z intuicją, ale rozważ to: komputer będzie w porządku, jeśli umieścisz cały kod w jednej metodzie, użyjesz fragmentu pamięci współdzielonej do przechowywania wartości tymczasowych i użyjesz sporej liczby instrukcji skoku. Co więcej, czasami robią to kompilatory na etapie optymalizacji. Jednak ludzki mózg nie radzi sobie łatwo z takim podejściem.
Programowanie funkcjonalne zmusza do pisania czystych funkcji bez efektów ubocznych, z silnymi typami, w ekspresyjny sposób. W ten sposób znacznie łatwiej jest wnioskować o funkcji, ponieważ jedyną rzeczą, jaką produkuje, jest jej wartość zwracana. Odcinek podcastu Programming Throwdown Programowanie funkcyjne z Adamem Gordonem Bellem pomoże ci zdobyć podstawową wiedzę i będziesz mógł kontynuować z odcinkami Corecursive: Boski język programowania z Philipem Wadlerem i teoria kategorii z Bartoszem Milewskim. Dwie ostatnie znacznie wzbogaciły moje postrzeganie programowania.
Rozwój oparty na testach
Polecam opanowanie TDD. Najlepszym sposobem na naukę jest praktyka. Kalkulator ciągów Kata to świetny sposób na ćwiczenie z kodem kata. Opanowanie kata zajmie trochę czasu, ale ostatecznie pozwoli ci w pełni wchłonąć ideę TDD, co pomoże ci stworzyć dobrze ustrukturyzowany kod, z którym praca jest przyjemnością, a także testowalny.
Jedna uwaga: czasami zobaczysz purystów TDD twierdzących, że TDD to jedyny właściwy sposób programowania. Moim zdaniem to po prostu kolejne przydatne narzędzie w twoim zestawie narzędzi, nic więcej.
Czasami trzeba zobaczyć, jak dopasować moduły i procesy względem siebie i nie wiedzieć, jakich danych i sygnatur użyć. W takich przypadkach pisz kod do momentu skompilowania, a następnie napisz testy, aby rozwiązać problemy i debugować funkcjonalność.
W innych przypadkach znasz dane wejściowe i wyjściowe, ale nie masz pojęcia, jak poprawnie napisać implementację ze względu na skomplikowaną logikę. W takich przypadkach łatwiej jest zacząć postępować zgodnie z procedurą TDD i budować kod krok po kroku, niż spędzać czas na myśleniu o idealnej implementacji.
7. Utrzymuj proste i skoncentrowane testy
Praca w starannie zorganizowanym środowisku kodu bez zbędnych zakłóceń to przyjemność. Dlatego ważne jest, aby w testach stosować zasady SOLID, KISS i DRY — korzystając z refaktoryzacji, gdy jest to potrzebne.
Czasami słyszę komentarze typu: „Nienawidzę pracy w mocno przetestowanej bazie kodu, ponieważ każda zmiana wymaga ode mnie naprawienia dziesiątek testów”. Jest to problem wymagający dużej konserwacji spowodowany testami, które nie są skoncentrowane i próbują testować zbyt wiele. Zasada „Zrób jedną rzecz dobrze” dotyczy również testów: „Przetestuj jedną rzecz dobrze”; każdy test powinien być stosunkowo krótki i testować tylko jedną koncepcję. „Dobrze przetestuj jedną rzecz” nie oznacza, że powinieneś być ograniczony do jednego potwierdzenia na test: możesz użyć dziesiątek, jeśli testujesz nietrywialne i ważne mapowanie danych.
To skupienie nie ogranicza się do jednego konkretnego testu lub rodzaju testu. Wyobraź sobie, że masz do czynienia ze skomplikowaną logiką, którą przetestowałeś za pomocą testów jednostkowych, takich jak mapowanie danych z systemu ERP do Twojej struktury, i masz test integracyjny, który uzyskuje dostęp do próbnych interfejsów API ERP i zwraca wynik. W takim przypadku ważne jest, aby pamiętać, co już obejmuje test jednostkowy, aby nie testować mapowania ponownie w testach integracyjnych. Zwykle wystarczy upewnić się, że wynik ma prawidłowe pole identyfikacyjne.
Dzięki strukturze kodu przypominającej klocki Lego i ukierunkowanym testom zmiany w logice biznesowej nie powinny być bolesne. Jeśli zmiany są radykalne, po prostu usuwasz plik i powiązane z nim testy i tworzysz nową implementację z nowymi testami. W przypadku drobnych zmian zwykle zmieniasz od jednego do trzech testów, aby spełnić nowe wymagania i wprowadzić zmiany w logice. Zmiana testów jest w porządku; możesz myśleć o tej praktyce jako o prowadzeniu ksiąg rachunkowych z podwójnym zapisem.
Inne sposoby na osiągnięcie prostoty to:
- Wymyślanie konwencji strukturyzacji plików testowych, strukturyzacji treści testowych (zwykle struktura Arrange-Act-Assert) i nazewnictwa testów; następnie, co najważniejsze, konsekwentne przestrzeganie tych zasad.
- Wyodrębnianie dużych bloków kodu do metod takich jak „przygotuj żądanie” i tworzenie funkcji pomocniczych dla powtarzających się akcji.
- Stosowanie wzorca konstruktora do konfiguracji danych testowych.
- Używanie (w testach integracyjnych) tego samego kontenera DI, którego używasz w głównej aplikacji, więc każde wystąpienie będzie tak trywialne jak
TestServices.Get()bez ręcznego tworzenia zależności. W ten sposób łatwo będzie czytać, utrzymywać i pisać nowe testy, ponieważ masz już przydatnych pomocników.
Jeśli czujesz, że test staje się zbyt skomplikowany, po prostu zatrzymaj się i pomyśl. Albo moduł, albo test wymaga refaktoryzacji.
8. Użyj narzędzi, aby ułatwić sobie życie
Podczas testów staniesz przed wieloma żmudnymi zadaniami. Na przykład konfigurowanie środowisk testowych lub obiektów danych, konfigurowanie kodów pośredniczących i mocków dla zależności i tak dalej. Na szczęście każdy dojrzały stos technologii zawiera kilka narzędzi, dzięki którym te zadania są znacznie mniej nużące.
Sugeruję, abyś napisał swoje pierwsze sto testów, jeśli jeszcze tego nie zrobiłeś, a następnie zainwestuj trochę czasu w zidentyfikowanie powtarzających się zadań i poznanie narzędzi związanych z testowaniem dla twojego stosu technologicznego.
Aby uzyskać inspirację, oto kilka narzędzi, których możesz użyć:
- Przetestuj biegaczy. Poszukaj zwięzłej składni i łatwości użycia. Z mojego doświadczenia, dla .NET, polecam xUnit (choć NUnit też jest dobrym wyborem). W przypadku JavaScript lub TypeScript wybieram Jest. Spróbuj znaleźć najlepsze dopasowanie do swoich zadań i sposobu myślenia, ponieważ narzędzia i wyzwania ewoluują.
- Wyśmiewanie bibliotek . Mogą istnieć makiety niskiego poziomu dla zależności kodu, takie jak interfejsy, ale istnieją również makiety wyższego poziomu dla internetowych interfejsów API lub baz danych. W przypadku JavaScript i TypeScript makiety niskiego poziomu zawarte w Jest są w porządku. Dla platformy .NET. Używam Moq, chociaż NSubstitute też jest świetny. Jeśli chodzi o mocki webowego API, lubię używać WireMock.NET. Może być używany zamiast interfejsu API do rozwiązywania problemów i debugowania obsługi odpowiedzi. Jest również bardzo niezawodny i szybki w testach automatycznych. Bazy danych można było wyśmiewać przy użyciu ich odpowiedników w pamięci. EfCore w .NET udostępnia taką opcję.
- Biblioteki generowania danych . Te narzędzia wypełniają obiekty danych losowymi danymi. Przydają się, gdy na przykład zależy Ci tylko na kilku polach z obiektu transferu dużych zbiorów danych (jeśli tak, to może chcesz tylko przetestować poprawność mapowania). Możesz je wykorzystać do testów, a także jako losowe dane do wyświetlenia w formularzu lub do uzupełnienia bazy danych. Do celów testowych używam AutoFixture w .NET.
- Biblioteki automatyzacji interfejsu użytkownika . Są to zautomatyzowani użytkownicy do automatycznych testów: mogą uruchamiać Twoją aplikację, wypełniać formularze, klikać przyciski, czytać etykiety i tak dalej. Aby poruszać się po wszystkich elementach aplikacji, nie musisz zajmować się klikaniem według współrzędnych lub rozpoznawaniem obrazu; główne platformy mają narzędzia do wyszukiwania potrzebnych elementów według typu, identyfikatora lub danych, dzięki czemu nie musisz zmieniać testów przy każdym przeprojektowaniu. Są solidne, więc gdy już sprawisz, że będą działać dla Ciebie i CI (czasami dowiesz się, że wszystko działa tylko na Twoim komputerze ), będą działać dalej. Lubię używać FlaUI dla .NET i Cypress dla JavaScript i TypeScript.
- Biblioteki asercji . Większość programów uruchamiających testy zawiera narzędzia asercji, ale istnieją przypadki, w których niezależne narzędzie może pomóc w pisaniu złożonych asercji przy użyciu czystszej i bardziej czytelnej składni, takiej jak Fluent Assertions dla platformy .NET. Szczególnie podoba mi się funkcja zapewniająca, że kolekcje są równe, niezależnie od kolejności elementu lub jego adresu w pamięci.
Niech przepływ będzie z tobą
Szczęście jest ściśle powiązane z tak zwanym doświadczeniem „flow” opisanym szczegółowo w książce Flow: The Psychology of Optimal Experience . Aby osiągnąć ten przepływ, musisz być zaangażowany w działanie z jasnym zestawem celów i być w stanie zobaczyć swoje postępy. Zadania powinny skutkować natychmiastową informacją zwrotną, do której idealne są testy automatyczne. Musisz także zachować równowagę między wyzwaniami a umiejętnościami, która zależy od każdej osoby. Testy, szczególnie w przypadku podejścia z TDD, mogą pomóc Ci w prowadzeniu i wzbudzić zaufanie. Pomagają w wyznaczaniu konkretnych celów, a każdy zdany test jest wskaźnikiem Twoich postępów.
Właściwe podejście do testowania może sprawić, że będziesz szczęśliwszy i bardziej produktywny, a testy zmniejszą ryzyko wypalenia. Kluczem jest postrzeganie testowania jako narzędzia (lub zestawu narzędzi), które może pomóc w codziennej rutynie programistycznej, a nie jako uciążliwego kroku w zabezpieczaniu kodu w przyszłości.
Testowanie jest niezbędną częścią programowania, która pozwala inżynierom oprogramowania usprawnić sposób pracy, zapewnić najlepsze wyniki i optymalnie wykorzystać swój czas. Co być może nawet ważniejsze, testy mogą pomóc programistom bardziej cieszyć się ich pracą, zwiększając w ten sposób ich morale i motywację.
