Testowanie Automatyczne w JavaScript — teoria i przykłady
Testowanie Automatyczne i JavaScript.
Poniższy post jest dopełnieniem dwóch prezentacji dotyczących Testowania Automatycznego w JavaScript.
Oczym mówię i koduje:
👉 Testy Jednostkowe — #Jest
👉 Testy Strukturalne i ich konfiguracja, z użyciem narzędzi #codeclimate #eslint
👉 Testy Integracyjne/funkcjonalne-jest i #react-testing-library
👉 Testy End-to-end e2d — #cypress.js
👉 Testy Akceptacyjne #codecept.ja
👉 Konfiguracja #Jest do Test Driven Development, raporty Code Coverage #junit
👉 Konfiguracja Continuous Integration na podstawie #CircleCi, #Jest i #Cypress
👉 Ustawienia pre-hooków z użyciem husky, aby nie wypychać niepoprawnego kodu.
Oprócz poniższego postu całość dostępna jest w formie repozytorium github oraz prezentacji wideo:
Cześć pierwsza — teoria
https://lnkd.in/eTX-KzX
Cześć druga — Livecoding
Testowanie Automatyczne w JavaScript — teoria i przykłady
Pisanie testów to wykrywanie błędów przez programistów w trakcie pisania kodu źródłowego zamiast w najgorszym możliwym momencie czy wykryciu błędu na produkcji przez klienta.
Mateusz Wojczal 2020
Typy testów
Nie ma jasnego podziału typów testów. Często są mylone grupy i typy. Oprócz podziału na uruchamiane ręczne
lub automatyczne
są jeszcze:
Strukturalne
- testy białej skrzynki, skupiają się tylko i wyłącznie na testowaniu kodu bądź systemu. Są one przeciwieństwem testów czarnej skrzynki.Niefunkcjonalne
- testowanie atrybutów modułu lub systemu, które nie odnoszą się do jego funkcjonalności, np. niezawodności, efektywności, pielęgnowalności i przenaszalności.Funkcjonalne
- testy czarnej skrzynki, oparte na analizie specyfikacji funkcjonalnej modułu lub systemu.
plus Testy Regresji
, Wizualne
(snapshots
) i inne.
Testy jednostkowe
Testy jednostkowe są przeprowadzane na bardzo niskim poziomie aplikacji, bardzo zbliżonym do kodu źródłowego oprogramowania i polegają na testowaniu poszczególnych metod i funkcji klas, komponentów lub modułów wykorzystywanych w programie. Automatyzacja jest prosta i mogą one być bardzo szybko przeprowadzane przez serwer ciągłej integracji (jednocześnie).
Pokrycie kodu określa które części (linie kodu) programu zostały przetestowane przez testy jednostkowe.
Pokrycie kodu bardzo dobrze współpracuje z Continuous Integration
Pokrycie kodu bardzo dobrze współpracuje z Continuous Integration. Raport HTML jUnit
Następny przykład jako raport zmiany pokrycia w sekcji Merge Request.
Testy statyczne (standard kod.)
Test polega na automatycznym sprawdzaniu kodu w celu znalezienia błędów. Przykładowe narzędzia tj. sonarqube
, codeclimate
, eslint
itp.
Poniżej przykład testu analizy statycznej kodu źródłowego z użyciem eslint
$ eslint . --fix
/Users/qunabu/Desktop/localhost/pupile-web/src/actions/UserActions.js
33:26 error 'res' is defined but never used no-unused-vars
123:12 error 'res' is defined but never used no-unused-vars
139:12 error 'res' is defined but never used no-unused-vars
/Users/qunabu/Desktop/localhost/pupile-web/src/components/ArticlesPage/ArticlesPage.js
31:11 error Missing "key" prop for element in iterator react/jsx-key
/Users/qunabu/Desktop/localhost/pupile-web/src/reducers/Visits.js
100:7 error Unexpected lexical declaration in case block no-case-declarations
/Users/qunabu/Desktop/localhost/pupile-web/src/utils/utils.js
17:13 error Do not access Object.prototype method 'hasOwnProperty' from target object no-prototype-builtins
✖ 92 problems (92 errors, 0 warnings)
...
Oraz codeclimate
~ codeclimate analyze
Starting analysis
Running structure: Done!
Running duplication: Done!
Running eslint: Done!
== coverage/lcov-report/block-navigation.js (2 issues) ==
2-78: Function `init` has a Cognitive Complexity of 9 (exceeds 5 allowed). Consider refactoring. [structure]
2-78: Function `init` has 57 lines of code (exceeds 25 allowed). Consider refactoring. [structure]
== coverage/lcov-report/sorter.js (3 issues) ==
2-168: Function `addSorting` has a Cognitive Complexity of 38 (exceeds 5 allowed). Consider refactoring. [structure]
2-168: Function `addSorting` has 138 lines of code (exceeds 25 allowed). Consider refactoring. [structure]
126-157: Function `enableUI` has 26 lines of code (exceeds 25 allowed). Consider refactoring. [structure]
== src/actions/ArticleActions.js (1 issue) ==
19-29: Similar blocks of code found in 3 locations. Consider refactoring. [duplication]
...
- Wydajnościowe
- sprawdzimy obciążenie systemu. Jak długo trwa odpowiedź serwera itp. - Przeciążeniowe (część wydajnościowych)
- przy ograniczeniu bądź braku zasób tj: procesor, pamięć, dysk, itp. - Obciążeniowe
- przy zwiększonej ilości użytkowników, rekordów, itp. - Użyteczności (ux)
- łatwość korzystania z oprogramowania. - Pielęgnowalności
- łatwość modyfikacji i dostosowań do nowych wymagań. - Niezawodności
- wykonanie wymaganych funkcji w określonych warunkach. - Przenaszalności
- łatwość przeniesienia z jednego środowiska na drugie.
Testy integracyjne
Testy integracyjne sprawdzają, czy różne moduły lub usługi wykorzystywane przez oprogramowanie dobrze ze sobą współpracują. Tego poziomu testy mogą być stosowane na przykład w celu sprawdzania interakcji aplikacji z bazą danych lub upewnienia się, że mikro-usługi działają zgodnie z postawionymi wymaganiami i oczekiwaniami, wymagają one uruchomienia wielu elementów aplikacji.
Testy end-to-end (e2e)
Symulują zachowanie użytkownika korzystającego z oprogramowania w pełnym środowisku aplikacji, sprawdzają czy wszystkie elementy aplikacji działają zgodnie z założeniami.
Testy akceptacyjne (BDD)
Testy akceptacyjne są formalnymi testami oprogramowania przeprowadzanymi w celu sprawdzenia, czy dany system spełnia stawiane przed nim wymagania biznesowe. Wymagają one uruchomienia i poprawnego działania całości aplikacji i polegają na replikowaniu zachowań użytkowników. Tego poziomu testy mogą także obejmować nieco szerszy zakres, w który może wejść między innymi pomiar wydajności systemu oraz odrzucenie zmian w przypadku, gdy nie pozwalają one na osiągnięcie postawionych celów i wymagań.
Testy wizualne (snapshot)
Porównywanie wyniku uruchomienia funkcji z jakimś oczekiwanym efektem, zapisanego wcześniej w repozytorium jak wzorzec prawidłowego rezultatu.
Testy snapshot kodu.
Środowiska uruchomieniowe
aka Test Runners — środowiska w którym testy są uruchamiane i z którego API
korzystają. Runnery zazwyczaj posiadają podobne API
, np Jasimne
, Jest
i Mocha
.
Przygotowanie
Funkcjonalne testy wymagają przygotowania środowiska pod testy, np poprzez funkcję beforeEach
, beforeAll
, afterEach
i afterAll
.
Mocks (Moki)
Obiekty które udają inne serwisy, np. wysyłanie maili.
Udawanie zewnętrznego REST API
.
Faktorie (factories)
Służą do łatwego generowania skomplikowanych obiektów.
Zasady
Testy jednostkowa są wyizolowanie i niezależne od siebie.
- Każdą funkcjonalność należy określić w jednym i tylko jednym teście
- Wykonanie / kolejność uruchomienia jednego testu nie może wpływać na inne
- Kod ma być niezależny, gotowy do współbieżności (parallel computing)
- Powtarzalny
- Szybki
- Zwarty i logiczny
- Łatwy do pisania i czytania
- Testy jednostkowe to także kod źródłowy (poziom jakości, co testowany kod)
Kluczem do dobrego testu jednostkowego jest napisanie testowalnego kodu. Zastosowanie prostych zasad projektowania może pomóc w szczególności:
- Używaj dobrej konwencji nazewnictwa i komentuj swój kod („dlaczego?”, A nie „jak”) ale pamiętaj, że komentarze nie zastępują złego nazewnictwa lub złego projektu
- DRY: nie powtarzaj się, unikaj powielania kodu. Używaj abstrakcji. Pisz tzn
Helpery
- Pojedyncza odpowiedzialność: każdy przedmiot / funkcja musi skupiać się na jednym zadaniu (SOLID)
- Zachowaj jeden poziom abstrakcji w tym samym komponencie (na przykład nie mieszaj logiki biznesowej ze szczegółami technicznymi niższego poziomu w tej samej metodzie)
- Minimalizuj zależności między komponentami: hermetyzuj, wymieniaj mniej informacji między komponentami
- Konfigurowalność zamiast
hard-coding
, metaprogramowanie - co zapobiega konieczności replikowania dokładnie tego samego środowiska podczas testowania - Zastosuj odpowiednie wzorce projektowe, zwłaszcza wstrzyknięcie zależności (
dependency injection
), które pozwala oddzielić odpowiedzialność za tworzenie obiektu od logiki biznesowej. - Unikaj globalnego stanu zmiennego
Powyższe zasady nie odnoszą się tylko do testowania ale ogólnie do profesjonalnego programowania — powyższe porady nierzadko zaczerpnięte są z takich definicji jak programowanie funkcyjne , KISS, DRY lub SOLID.
Test Driven Development. Tam gdzie to możliwe używaj TDD
Test Driven Development to proces projektowania, a nie proces testowania. TDD to solidna metoda interaktywnego projektowania komponentów oprogramowania („jednostek”), tak aby ich zachowanie było określane za pomocą testów jednostkowych.
Pierwszy cykl testowy
- Napisz prosty test zakończony niepowodzeniem
- Niech test przejdzie, wpisując minimalną ilość kodu, nie przejmuj się jakością kodu
- Refaktoryzuj kod, stosując zasady / wzorce projektowe
Konsekwencje pierwszego cyklu testowego
- Napisanie testu najpierw sprawia, że projekt kodu jest de facto testowalny
- Pisanie tylko takiej ilości kodu potrzebnego do zaimplementowania wymaganej funkcjonalności sprawia, że wynikowa baza kodu jest minimalna, a tym samym łatwiejsza w utrzymaniu
- Bazę kodu można rozbudować za pomocą mechanizmów refaktoryzacji, testy dają pewność, że nowy kod nie modyfikuje istniejących funkcjonalności
- Czyszczenie kodu w każdym cyklu sprawia, że baza kodu jest łatwiejsza w utrzymaniu, znacznie tańsza jest częsta zmiana kodu w małych przyrostach
- Szybka informacja zwrotna dla programistów, wiesz, że nic nie psujesz i że ewoluujesz system w dobrym kierunku
- Zapewnia pewność dodawania funkcji, naprawiania błędów lub odkrywania nowych projektów
- Kod napisany bez podejścia
test-first
jest często bardzo trudny do przetestowania.
Dobre praktyki pisania testów
Strukturyzuj/grupuj testy
nie ustrukturyzowany kod ☹️
lepiej 😁
Nazewnictwo
Nazwij swoje testy poprawnie — zwięzłe, jednoznacznie, opisowo i poprawnie w języku angielskim. Przeczytaj dane wyjściowe programu uruchamiającego specyfikację i sprawdź, czy jest zrozumiałe! Pamiętaj, że ktoś inny też to przeczyta. Testy mają być żywą dokumentacją kodu.
nic nie mówiące nazwy ☹️
bardziej opisujące przypadek nazewnictwo 😁
Dobra praktyka w nazywaniu testów 😁
Unikaj logiki
Zawsze używaj prostych instrukcji (expressions
). Nie używaj pętli i / lub warunków. Jeśli to zrobisz, dodajesz możliwy punkt wejścia dla błędów w samym teście:
- Warunki (
Conditions
): nie wiesz, jaką ścieżką przejdzie test (źle) - Pętle (
Loops
): możesz udostępniać stan między testami (źle)
brzydko-stosowanie logiki w teście☹️
w momencie w który powyższy test zwróci błąd dostaniesz informację, że błąd wystąpił w linii 12, ale już odszukanie który konkretnie był to przypadek będzie trudne. O wiele lepiej jest napisać tak 😁
Najlepiej: test dla każdego przypadku. Zapewni ładny raport wszystkich możliwych przypadków, poprawiając łatwość konserwacji. 😁😁😁
Nie pisz zbędnych oczekiwań (expect)
Pamiętaj, testy jednostkowe to specyfikacja projektu określająca, jak powinno działać określone zachowanie, a nie lista obserwacji wszystkiego, co dzieje się w kodzie.
Ten kod sprawdza implementację a nie działanie ☹️
o wiele lepiej jest 😁
Poprawi to łatwość konserwacji ( maintainability
). Twój test nie jest już powiązany ze szczegółami implementacji.
Prawidłowo skonfiguruj działania, które mają zastosowanie do wszystkich testów
Działania przygotowujące testy nie powinny być treścią samego testu.
brzydko ☹️
lepszy kod — testy skupiają się na swojej treści 😁
Zwróć uwagę gdzie powinien być wywołany jasmine.Ajax.install();
Znaj na wylot testowy framework
brzydko ☹️
o wiele lepiej jest korzystać z API
runnera 😁
Nie sprawdzaj wielu problemów w tym samym teście
Jeśli metoda ma kilka wyników końcowych, każdą z nich należy przetestować oddzielnie. W każdym przypadku, gdy pojawi się błąd, pomoże ci zlokalizować źródło problemu w dokładnym miejscu.
Pisanie „AND
” lub „OR
” podczas nazwania testu oznacza raczej refaktoryzację testu.
brzydko — test chce za dużo sprawdzać☹️
lepiej, rozdzielamy powyższe na osobne testy 😁
Pokryj przypadek ogólny i wszystkie brzegowe
Nieprzewidziane zachowanie zwykle zdarza się na krawędziach ( edge case
) - Pamiętaj, że testy mają być żywą dokumentacją kodu. Przykład testowania Odwrotnej Polskiej Notacji.
ten test nie sprawdza wszystkiego ☹️
ten test sprawdza o wiele więcej 😁 — przypadki brzegowe dla RPN
Stosując TDD, zawsze zaczynaj od napisania najprostszego testu zakończonego niepowodzeniem
Twój pierwszy test raczej nie powinien wyglądać tak ☹️
a raczej tak 😁
Od tego momentu zacznij stopniowo dodawać funkcjonalności, np taką z powyższego testu.
Testuj zachowanie, a nie implementację
brzydko ☹️ ponieważ implementacja może ulec zmianie w przyszłości
o wiele lepiej przy okazji testujesz API
)😁
Twórz nowe testy dla każdej usterki
Zawsze, gdy zostanie znaleziony błąd, przed poprawieniem kodu utwórz test, który powiela problem. Następnie zastosuj TDD, aby to naprawić.
Nie pisz testów jednostkowych dla złożonych interakcji użytkownika
Tym zajmują się testy funkcyjne.
Przetestuj proste działania użytkownika
Przykład prostych działań użytkownika:
- Kliknięcie linku, który przełącza widoczność elementu
DOM
- Przesłanie formularza, który uruchamia walidację formularza
Najpierw czytaj kod testowy
Przeglądając (nowy) kod, zawsze zaczynaj od przeczytania kodu testów. Testy to małe przypadki użycia kodu, do których można przejść.
Pomoże ci to bardzo szybko zrozumieć zamiary dewelopera (wystarczy spojrzeć na nazwy testów) oraz uzupełnia to dokumentację techniczną projektu — w niektórych przypadkach jest to jedyna dostępna dokumentacja.
LiveCoding
#LiveCoding: testowanie prostej aplikacji napisanej w #React z #create-react-app.
- LiveCoding jako nagrany screencast dostępny jest na YouTube — https://www.youtube.com/watch?v=tjo_oJm6pJU
- Kod na którym oparte jest coding dostępny jest w repozytorium https://github.com/qunabu/js-testing-types
Kod w tym repozytorium jest oparty o React
i domyślne ustawienia create-react-app
, oprócz tego zawiera:
- Prostą aplikację do której podłączone są testy.
- Runner do testów
Jest
. Testy jednostkowe i integracyjne w plikach**/*.test.js
- Framework do testowania komponentów Reacta react-testing-library
- Runner do testów end-to-end (e2e) cypress. Definicje testów w katalogu cypress
- Pre hook git-push uruchamiający testy w momecie
git push
. Kod który nie przechodzi testówJest
nie będzie wysłany do repozytorium. - Skonfigurowany Continuous Integration oparty o CircleCi
- Runner do testów akceptacyjnych i BDD (Behaviour Driven Development) https://codecept.io/
Korzystałem z otwartych materiałów, tj
- A guide to unit testing in JavaScript. Marc Mignonsin
- Jest is a delightful JavaScript Testing Framework
- create-react-app
- Cypress Test Runner
- CodeceptJS — SuperCharged End 2 End Testing with WebDriver & Puppeteer
Mateusz Wojczal 2020
https://mateusz.wojczal.com
Originally published at https://github.com.