Testowanie Automatyczne w JavaScript — teoria i przykłady

Mateusz Wojczal
9 min readNov 26, 2020

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

Testowanie Automatyczne w JavaScript — teoria i przykłady

Mateusz Wojczal i wsparcie Strajku Kobiet

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).

Banalny przykład testu jednostkowego

Pokrycie kodu określa które części (linie kodu) programu zostały przetestowane przez testy jednostkowe.

Jest Code Coverage. Procentowe pokrycie kodu

Pokrycie kodu bardzo dobrze współpracuje z Continuous Integration

Fragmenty kodu bez pokrycia testami. Gitlab
Rezultat działa statycznej analizy kodu. Narzędzie Code Climate. Gitlab

Pokrycie kodu bardzo dobrze współpracuje z Continuous Integration. Raport HTML jUnit

Raport Jest Code Coverage w postaci raportu HTML

Następny przykład jako raport zmiany pokrycia w sekcji Merge Request.

Zmiana (-2.45%) procentowego pokrycia kodu w Merge Request. Gitlab

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.

Przykład testu integracyjnego. Jest, Testowanie API

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.

Test e2e. Cypress

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ń.

Scenariusz testu akceptacyjnego. Codecept.js

Testy wizualne (snapshot)

Test porównujący zmianę kodu w kontekście wizualnym. Percy

Porównywanie wyniku uruchomienia funkcji z jakimś oczekiwanym efektem, zapisanego wcześniej w repozytorium jak wzorzec prawidłowego rezultatu.
Testy snapshot kodu.

Jest Snapshot Test

Ś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.

Porównanie środowisk uruchomieniowych tzw test-runnerów

Przygotowanie

Funkcjonalne testy wymagają przygotowania środowiska pod testy, np poprzez funkcję beforeEach, beforeAll, afterEach i afterAll.

Przygotowanie środowiska do testów

Mocks (Moki)

Obiekty które udają inne serwisy, np. wysyłanie maili.

Przykład mockowania wysyłki 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

  1. Napisz prosty test zakończony niepowodzeniem
  2. Niech test przejdzie, wpisując minimalną ilość kodu, nie przejmuj się jakością kodu
  3. 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.

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ów Jest 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

Mateusz Wojczal 2020
https://mateusz.wojczal.com

Originally published at https://github.com.

--

--

Mateusz Wojczal

founder of Qunabu Interactive from Gdańsk, Poland. Full-stack web developer with over a dozen years of experience.