Testowanie aplikacji

Find a bug

23 lutego 2016 19:30
Piotr Bajsarowicz
python development unittest

## Idea testowania

Wyobraź sobie, że pracujesz w projekcie, który składa się z 20 odrębnych aplikacji, gdzie każda składa się z kilku modułów. Projekt dostarcza aplikacjom zewnętrznym programistyczny interfejs (API) w dwóch wersjach, które ciągle są rozwijane - v1 SOAP, v2 - REST.

Twoje zadanie polega na edycji endpointu, dostarczającego dane wykonanych transakcji dla opcji opóźnionej dostawy, dla klientów korzystających z oferty leasingowej.

Masz do wyboru dwa scenariusze:

"This is your last chance. After this, there is no turning back."

Scenariusz "You take the blue pill"

Od początku projektu nikt nie zwracał uwagi na testowanie oprogramowania. Robili to tylko developerzy. Więc i w tym przypadku, wykonujesz tylko testy manualne (smoke testing) i w pełnym stresie oddajesz swoje zadanie, które jest deployowane od razu na instancji produkcyjnej i prosisz swojego ulubionego boga o to, aby wszystko działało poprawnie.

Scenariusz "You take the red pill"

Każdy z modułów z projekcie posiada testy jednostkowe, dlatego po zakończeniu swojej pracy uruchamiasz je sprawdzając czy wprowadzone przez Ciebie zmiany wpłynęły w oczekiwany sposób na działanie poszczegółnych modułów oraz na działanie samego endpointu. Dostrzegasz, że testy dla tego konkretnego endpointu uległy zmianie, i zakończyły się błędem, niemniej jest to oczekiwany efekt, co tylko potwierdza poprawność wykonaniej pracy. Dla stuprocentowej pewności aktualizujesz istniejący test, sprawdzając czy nowa wersja działa zgodnie z planem. Dodatkowo implementujesz kolejne testy, realizujące odrębne scenariusze, zarówno pozytywne jak i te, które powinny zakończyć się błędem. Następnie swój kod oddajesz w postaci pull requesta do code review, gdzie inni developrzy sprawdzając jakość kodu, jego zgodność z zasadami stylu kodu panującymi w projekcie oraz zgodność z logiką biznesową. Jeśli wszystko jest poprawnie to Twoja praca ląduje w rękach testera (Quality Assurance Engineer), który sprawdza wszelkie możliwe scenariusze testowe. Dopiero po jego weryfikacji zadanie uznaje się za zakończone. Po pewnym, z góry określonym czasie, zadanie trafia na instancję, która ma być kolejną wersją produkcji (release), na której to przeprowadzane są całościowe testy aplikacji (regression testing).

"Remember: all I'm offering is the truth. Nothing more."

Którą drogę wybierasz?

Wybór wydaje się oczywisty... JEDNAKŻE historia zna wiele przypadków, w których to testowanie (szczególnie testy jednostkowe) jest celowo pomijane. Tłumaczone jest to brakiem czasu, niepotrzebnym zangażowaniem pracy programisty, brakiem poczucia potrzeby testowania. Jest to bardzo złudne.

Testowanie aplikacji się opłaca - to nie marnowanie czasu.

Efektywne testowanie oprogramowania pozwala uniknąć wielu potencjalnych problemów. Dzięki testom jednostkowym w bardzo łatwy sposób można zabezpieczyć się przed występieniem błędów w kodzie źródłowym aplikacji (bug).

Bez testów - "Welcome to the desert of the real."

Weryfikacja a walidacja

Aplikacje tworzymy na podstawie specyfikacji określonych przez klienta, aby produkt końcowy był zgody z jego oczekiwaniami, korzystając z procesu weryfikacji oraz walidacji.

Weryfikacja: Czy budujemy prawidłowo produkt?

Walidacja: Czy budujemy prawidłowy produkt?

Weryfikacja: Czy budujemy prawidłowo produkt? Zdecydowanie TAK! Most umożliwia przemieszczenie się z jednej strony rzeki na drugą.

Walidacja: Czy budujemy prawidłowy produkt? Zdecydowanie NIE! Most nie jest bezpieczny, jego konstrukcja pozostawia wiele do życzenia. Klient zdecydowanie nie chce prowadzić statystyk (nie)udanych przejść.

Weryfikacja statyczna a dynamiczna

Weryfikacja statyczna to inspekcja kodu (code review). Developer kończąc swą pracę, oddaje ją grupie developerów do sprawdzenia.

Weryfikacja dynamiczna to proces polegający na sprawdzeniu czy zaimplementowana funkcjonalność działa zgodnie z oczekiwaniami. Tester (Quality Assurance Engineer) poddaje oddany kod wielu testom, realizując możliwie najwięcej scenariuszy, zarówno kończących się sukcesem jak i oczekiwanym błędem.

Czym jest testowanie

Testowanie oprogramowania jest wykonaniem kodu dla kombinacji danych wejściowych i stanów w celu wykrycia błędów

Udany test = wykrycie błędu

Efektywność testu = zdolność do znajdywania błędów

Ograniczenia testowania

"Testowanie może ujawnić obecność błędów, ale nigdy ich braku" (Dijkstra)

Testowanie oprogramowania nie daje stuprocentowej pewności, że w aplikacji nie występują żadne błędy.

"Mój wyrób spełni założenia, jeśli spełni je każda z jego części składowych i jeśli zmontuję je właściwie" (Weyuker)

Nie można założyć, że z poszczególnych poprawnych części zawsze powstaje poprawna całość.

Pokrycie kodu

Miara opisująca stopień w jakim kod źródłowy programu jest otestowany przez konkretny zestaw testów.

Istnieje narzędzia, które pomagają mierzyć pokrycie testami, np. coverage

TDD

Test-driven development to technika wytwarzania oprogramowania, w której najpierw implementowane są testy automatyczne a dopiero później funkcjonalność. Jeśli napisany kod przechodzi pozytywnie testy, programista przechodzi do refaktoryzacji aby kod spełniał wszystkie oczekiwane standardy.

Testowanie mutacyjne

Jest to metoda, pozwalająca na sprawdzenie efektywności testu. Polega ona na prostej modyfikacji kodu i sprawdzeniu jak zareagują na nie testy. Testy można uznać za efektywne, jeśli wykryją one tę mutację.

Rodzaje testów

TESTY JEDNOSTKOWE CEL: sprawdzenie pojedynczej jednostki oprogramowania jaką jest klasa, metoda, czy też zbiór współpracujących ze sobą klas

TESTY INTEGRACYJNE CEL: wykrycie błędów w interfejsach i interakcjach pomiędzy modułami.

TESTY SYSTEMOWE CEL: sprawdzenie czy system jako całość spełnia wymagania funkcjonalne i jakościowe postawione przez klienta.

TESTY AKCEPTACYJNE CEL: sprawdzenie czy system spełnia oczekiwania klienta. Testy przeprowadzane są w środowisku docelowym lub jak najbardziej zbliżonym do niego.

TESTY REGRESYJNE CEL: po modyfikacji systemu, przeprowadzenie testów, które przeszły pozytywnie proces weryfikacji przed zmianami.

Testowanie a debugowanie

Testowanie koncentruje się na znajdywaniu błędów, debugowanie zajmuje się ich lokalizacją i usuwaniem

Testowanie w praktyce

W ramach nauki pisania testów jednostkowych (unit testów) przejdziemy poniżej przez kilka przykładów.

Pierwszą rzeczą z jaką należy się zapoznać jest przede wszystkim to jak uruchomić unit testy znajdujące się w jakimś pliku. Załóżmy, że plik z naszymi testami nazywa się tests.py - w takim wypadku możemy je włączyć poleceniem (zwróćmy uwagę na brak rozszerzenia w komendzie):

$ python -m unittest tests

Przejdźmy jednak dalej do pisania samych unit testów. Pierwszy przykładem jaki sobie umówimy będzie test funkcji sumującej 2 liczby:

import unittest

def add(a, b):
    return a + b


class PyPilaTestCase(unittest.TestCase):

    def test_add(self):
        self.assertEqual(add(2, 3), 5)

Co jest ważne - klasa zawierająca unit testy musi dziedziczyć z unittest.TestCase oraz metody w niej które są testami muszą zaczynać się od test. W innym wypadku zostaną one pominięte. Funkcja assertEqual porównuje nam 2 argumenty które do niej przekażemy i jeżeli nie są równe to rzuca nam wyjątek AssertionError (co oznacza nieudany test).

Przejdźmy do innego równie prostego przykładu:

import unittest

def is_positive(number):
    return number > 0


class PyPilaTestCase(unittest.TestCase):

    def test_is_positive_true(self):
        self.assertTrue(is_positive(2))

    def test_is_positive_false(self):
        self.assertFalse(is_positive(-2))

Użyte w przykładzie metody assertFalse oraz assertTrue są jedynie skrótami od assertEqual(..., True) oraz odpowiednio assertEqual(..., False).

Ale zaraz, zaraz... Wróćmy na chwilę do pierwszego przykładu. Mamy tam niewinną funkcję dodającą dwie liczby, ale przeprowadźmy taki test:

import unittest

def add(a, b):
    return a + b


class PyPilaTestCase(unittest.TestCase):

    def test_add(self):
        self.assertEqual(add(0.1, 0.2), 0.3)

i co widzimy po uruchomieniu?

F
======================================================================
FAIL: test_add (tests.PyPilaTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests.py", line 10, in test_add
    self.assertEqual(0.1 + 0.2, 0.3)
AssertionError: 0.30000000000000004 != 0.3

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

enter image description here

Nie będziemy zagłębiać dlaczego tak się dzieje (wynika to z pewnych ograniczeń arytmetyki liczb zmiennoprzecinkowych w komputerze), jednakże jeżeli ktoś jest zainteresowany to polecam zapoznać się z http://stackoverflow.com/questions/588004/is-floating-point-math-broken

Ale jak to naprawić? Z pomocą przychodzi nam funkcja porównująca assertAlmostEqual, tzn. :

import unittest

def add(a, b):
    return a + b


class PyPilaTestCase(unittest.TestCase):

    def test_add(self):
        self.assertAlmostEqual(add(0.1, 0.2), 0.3)

Porównuje ona liczby tylko do pewnej precyzji (standardowo 7 miejsc po przecinku), dzięki czemu wynik naszego testu jest poprawny.

Nadal mało?

No dobra dobra... ale co jeśli mam funkcję która robi coś w odniesieniu do czasu, np. sprawdza czy obecna data jest dalsza niż 23.02.2017 r.? Musiałbym czekać cały rok żeby sprawdzić jej poprawność... Tutaj z pomocją przychodzą nam Mocki. Umożliwiają one zmianę zwracanej przez funkcję wartości niezależnie od jej argumentu na taką jaką akurat potrzebuje programista. Akurat przykład z datą jest nienajlepszy w przypadku pythona, ponieważ nieco ciężej jest zmockować wbudowane w pythona biblioteki (co nie znaczy, że jest to niemożliwe - http://stackoverflow.com/questions/4481954/python-trying-to-mock-datetime-date-today-but-not-working), jednakże jest to również temat z którym polecam się zapoznać.

Ćwiczenie

Na tym repozytorium możemy znaleźć dwa pliki z funkcjami - jeden z nich (v1) zawiera w 100% działające funkcje, natomiast w drugim (v2) wredny programista nieco później namieszał :). Zadanie polega na napisanie testów na podstawie pliku z pierwszą wersją, a następnie uruchomienie ich na wersji drugiej w celu wykrycia błędów (jest ich 4). Rozwiązanie do ćwiczenia można podsyłać na adres artur.grochowski@stxnext.pl w celu weryfikacji lub pytań.

Bibliografia

Comments