Nasza strona używa cookies. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Jak rozpocząć automatyzację testów w Pythonie - porównanie frameworków

Sprawdź porównanie 3 frameworków testowych Pythona: unittest, doctest oraz pytest i zdecyduj, który będzie najlepszy dla Ciebie do automatyzacji testów.

Po co automatyzacja? W niemal każdym projekcie software'owym przychodzi moment, w którym automatyzacja testów staje się priorytetem. Zarówno testerzy, jak i programiści, nie przepadają za powtarzalną pracą. Prowadzi ona do rutyny, znużenia, poczucia stagnacji, a w konsekwencji do ogólnego niezadowolenia. Choć testy manualne są nieodłączną i bardzo ważną czynnością w procesie rozwoju oprogramowania, to automatyzacja czynności, czy testów, które muszą być wykonane często, szybko i z jednakową dokładnością, może być kamieniem milowym naszego projektu.

Dzięki automatyzacji przyspieszymy proces testowania regresywnego, usprawnimy wczesną detekcję problemów, zyskamy czas dla testerów na usprawnienie działającego procesu testowego, na rozwój kompetencji itp. Oczywiście zalet automatyzacji jest dużo więcej, ale nie to jest tematem tego artykułu.

Mimo niewątpliwych korzyści, wprowadzenie automatyzacji testów do procesu rozwoju oprogramowania bywa dużym wyzwaniem. Najczęstszym problemem są niewystarczające kompetencje programistyczne lub ich brak u testerów, brak specjalistów od automatyzacji lub programistów, którzy mogliby się nią zająć. Kolejne pytania dotyczą wyboru konkretnego rozwiązania – komercyjne czy otwarte; proste narzędzia czy kompleksowy system? Choć wszystko w tym wypadku zależne jest od potrzeb projektu i wielu innych czynników, to często warto wybrać rozwiązania najprostsze – ogólnodostępne, darmowe rozwiązania – zwłaszcza do celów przygotowania koncepcji.

W tym przypadku język Python ma do zaoferowania kilka narzędzi i bibliotek, które pomogą stworzyć pierwsze testy automatyczne, bez większego wysiłku.


Wybór odpowiednich narzędzi

Wspominając narzędzia mam na myśli dobór odpowiedniego frameworka testowego oraz tzw. test runner. Framework na ogół ujmując definiuje strukturę naszego programu (testu) – jest zestawem reguł wskazujących sposób tworzenia testu automatycznego. Z natury powinien ułatwiać pisanie testów, redukując tym samym koszty automatyzacji. Test runner natomiast odpowiada za uruchamianie testów oraz odpowiednie raportowanie użytkownikowi statusów ich wykonania.

Na rynku jest sporo dostępnych frameworków testowych, jak i test runnerów, jednak skupimy się na kilku najpopularniejszych: unittest, doctest, pytest. To, który framework jest odpowiedni dla danego projektu zależy od wielu czynników – niemniej powinniśmy odpowiedzieć sobie na pytanie: jakie testy chcę automatyzować, jak je uruchamiać, jak raportować. Ponadto warto zwrócić uwagę na strukturę proponowaną przez dany framework – jej zalety i wady. Ostatecznie to przecież my będziemy później z tego narzędzia korzystać.


Unittest

Unittest jest standardową biblioteką Pythona. Możemy więc bez wstępnego wysiłku rozpocząć automatyzację testów, gdyż unittest to zarówno test framework, jak i test runner. Ponadto testy napisane z pomocą unittest łatwo adaptować pod inne frameworki, np. pytest. 

Aby rozpocząć pracę z unittest, należy wykonać kilka bazowych kroków:

  • zaimportować unittest z zestawu bibliotek standardowych;
  • stworzyć podklasę dla klasy TestCase z modułu unittest;
  • stworzyć metody nowo stworzonej klasy, które będą odpowiednikiem przypadków testowych;
  • celem weryfikacji wyników testów użyć metod self.assertEqual (odziedziczonych z klasy TestCase) – unittest nie wspiera wbudowanej instrukcji assert;
  • w głównym bloku programu wywołaj unittest.main().


Biorąc powyższe pod uwagę, można zaprezentować przykład testów weryfikujących poprawność działania prostej przykładowej funkcji, np. obliczającej silnię danej liczby:

import unittest

# Definicja testowanej funkcji
def funkcja_silni(n):
    if n < 0:
        return None
    if n < 2:
        return 1
    return n * funkcja_silni(n - 1)


class TestSilnia(unittest.TestCase):
    """
    Klasa testowa, dziedzicząca po klasie TestCase z modułu unittest.
    Zawiera testy weryfikujące poprawność działania funkcji silni.
    """

    def test_silnia_dodatnie(self):
        """ Sprawdź wynik silni dla liczby dodatniej >= 2. """
        self.assertEqual(funkcja_silni(5), 120, "Powinno być: 120")

    def test_silnia_ujemne(self):
        """ Sprawdź wynik silni dla liczby ujemnej. """
        self.assertEqual(funkcja_silni(-5), None, "Powinno być: None")

    def test_silnia_0(self):
        """ Sprawdź wynik silni dla liczby dodatniej, mniejszej niż
       2. """
        self.assertEqual(funkcja_silni(0), 1, "Powinno być: 1")

    def test_silnia_string(self):
        """ Sprawdź błąd funkcji silni dla wprowadzenia stringa. """
        self.assertRaises(TypeError, funkcja_silni, "0")


if __name__ == '__main__':
    unittest.main()


Wykonanie powyższego kodu wyświetli bardzo krótki raport wskazujący na przeprowadzenie 4 testów. Aby nieco rozwinąć raport, można użyć opcji -v, dzięki której raport uwzględni tzw. „docstring” dla każdej z metod testowych. Poniżej wynik wykonania naszych testów:

$ ./testClass.py -v
test_silnia_0 (__main__.TestSilnia)
Sprawdź wynik silni dla liczby dodatniej, mniejszej niż ... ok
test_silnia_dodatnie (__main__.TestSilnia)
Sprawdź wynik silni dla liczby dodatniej >= 2. ... ok
test_silnia_string (__main__.TestSilnia)
Sprawdź błąd funkcji silni dla wprowadzenia stringa. ... ok
test_silnia_ujemne (__main__.TestSilnia)
Sprawdź wynik silni dla liczby ujemnej. ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.004s

OK


Doctest

Kolejną ze standardowych bibliotek Pythona jest doctest. To interesująca pozycja na liście frameworków testowych z racji dość nietypowego podejścia do ich definiowania. Doctest bazuje bowiem na tzw. „docstringach” (choć nie tylko), czyli specjalnym typie komentarzy stanowiących swego rodzaju dokumentację naszego kodu. Doctest wyszukuje partii tekstu, które wyglądają jak interaktywna sesja Pythona, a następnie wykonuje je celem weryfikacji, czy działają tak, jak jest to opisane. 

Doctest może być wykorzystywany do wykonywania testów jednostkowych. Aby go użyć, wystarczy najpierw wywołać testowaną funkcję, wykorzystując odpowiednie dane testowe, a następnie wygenerowany output umieścić w docstringu dotyczącym testowanej funkcji. W ten sposób łatwo możemy wygenerować tekst, który później wykorzystywany będzie przez doctest celem weryfikacji, czy funkcja działa zgodnie z dokumentacją. 

Poniżej przykładowa adaptacja funkcji silni wraz z testami, tym razem jednak zbudowanych na bazie modułu doctest:

# Definicja testowanej funkcji
def funkcja_silni(n):
    """
    Sprawdź wynik silni dla liczby dodatniej >= 2.
    >>> funkcja_silni(5)
    120

    Sprawdź wynik silni dla liczby ujemnej.
    >>> funkcja_silni(-5)

    Sprawdź wynik silni dla liczby dodatniej, mniejszej niż 2.
    >>> funkcja_silni(0)
    1

    Sprawdź błąd funkcji silni dla wprowadzenia stringa.
    >>> funkcja_silni("5")
    Traceback (most recent call last):
    TypeError: '<' not supported between instances of 'str' and 'int'
    """
    if n < 0:
        return None
    if n < 2:
        return 1
    return n * funkcja_silni(n - 1)


if __name__ == "__main__":
    import doctest_example
    doctest_example.testmod()


Aby uruchomić testy, należy użyć doctest jako główny program wykonawczy za pomocą opcji -m. Doctest najczęściej nie generuje dodatkowego wyjścia (outputu), więc warto wykorzystać opcję -v celem zwiększenia ilości logów. Poniżej efekty wykonania naszych testów:

$ python -m doctest -v ./doctest_example.py
Trying:
    funkcja_silni(5)
Expecting:
    120
ok
Trying:
    funkcja_silni(-5)
Expecting nothing
ok
Trying:
    funkcja_silni(0)
Expecting:
    1
ok
Trying:
    funkcja_silni("5")
Expecting:
    Traceback (most recent call last):
    TypeError: '<' not supported between instances of 'str' and 'int'
ok
1 items had no tests:
    doctest_example
1 items passed all tests:
   4 tests in doctest_example.funkcja_silni
4 tests in 2 items.
4 passed and 0 failed.
Test passed.


PyTest

Ostatnim z omawianych frameworków jest PyTest – najbardziej kompleksowy w całym zestawieniu. Pozwala pisać proste i skalowalne przypadki testowe dla baz danych, API, czy nawet interfejsów graficznych (tzw. UI). Oznacza to, że nie ogranicza nas do testów jednostkowych, ale pozwala pisać kompleksowe testy funkcjonalne.

PyTest w przeciwieństwie do poprzedników nie jest zintegrowany z kanoniczną wersją Pythona. Oznacza to, że wymaga osobnej instalacji. Dla większości użytkowników Pythona nie powinien to być jednak problem – instalację frameworka załatwimy jedną komendą w konsoli:

pip install pytest

W zamian za tę pojedynczą linię otrzymujemy narzędzie posiadające mnóstwo zalet – w tym m.in.: prostota użycia z racji banalnej składni, możliwość zrównoleglenia wykonywania testów, możliwość uruchomienia konkretnego testu lub zestawu testów, automatyczną detekcję testów, możliwość pomijania wybranych testów. Wszystko to w dodatku na licencji open-source.

Aby rozpocząć pracę z Pytest, należy zapoznać się z definicją asercji oraz tym, jak PyTest rozpoznaje pliki i metody zawierające testy. Asercje to linie sprawdzające, czy zadane wyrażenie jest prawdziwe, czy fałszywe. Jeśli w konkretnej metodzie testowej wybrana asercja zwróci false, metoda jest przerywana (dalsze linie metody nie są wykonywane) i zapisywana jako test ze statusem FAIL (niepoprawne wykonanie / negatywny wynik testu). Następnie PyTest rozpoczyna wykonywanie kolejnej metody testowej.

Metody testowe wykrywane są wg swojej nazwy. Aby PyTest rozpoznał metodę jako test, jej nazwa musi rozpoczynać się od słowa „test”. Metody o nazwach nie wpasowujących się w ten schemat są przez PyTest ignorowane. W rozbudowanych projektach może zajść potrzeba rozdzielenia testów na kilka plików w różnych folderach. To też nie problem. Każdy plik z prefixem „test_” lub sufixem „_test” będzie rozpatrywany jako plik zawierający testy.

Rzecz jasna PyTest to framework oferujący wiele możliwości, więc nie jestem w stanie przedstawić go obszerniej w jednym artykule. Warto odnieść się tu bezpośrednio do dokumentacji, niemniej to już kwestia indywidualna. My skupimy się zatem na zaprezentowaniu PyTest, bazując na przykładzie funkcji silni, którą już dobrze znamy.

import pytest

# Definicja testowanej funkcji
def funkcja_silni(n):
    if n < 0:
        return None
    if n < 2:
        return 1
    return n * funkcja_silni(n - 1)

def test_silnia_dodatnie():
    """ Sprawdź wynik silni dla liczby dodatniej >= 2. """
    assert funkcja_silni(5) == 120, "Powinno być: 120"

def test_silnia_ujemne():
    """ Sprawdź wynik silni dla liczby ujemnej. """
    assert funkcja_silni(-5) == None, "Powinno być: None"

def test_silnia_0():
    """ Sprawdź wynik silni dla liczby dodatniej, mniejszej niż 2. """
    assert funkcja_silni(0) == 1, "Powinno być: 1"

def test_silnia_string():
    """ Sprawdź błąd funkcji silni dla wprowadzenia stringa. """
    with pytest.raises(TypeError):
        funkcja_silni("0")


Aby ururchomić testy, należy wykonać komendę: 

py.test -v <nazwa_pliku> 

Podanie nazwy pliku jest opcjonalne – w przypadku braku wskazania pliku, PyTest będzie szukał plików z sufixem _test lub prefixem test_. Po uruchomieniu kodu powyżej, otrzymałem następujące wyniki:

$ py.test -v pytestexample.py
=========================================================================
test session starts
=========================================================================
platform win32 -- Python 3.8.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- c:\path\to\interpreter\python.exe
cachedir: .pytest_cache
rootdir: C:\Path\To\Test
collected 4 items                                                                                                                                                                         

pytestexample.py::test_silnia_dodatnie PASSED                                                                                                                                       [ 25%]
pytestexample.py::test_silnia_ujemne PASSED                                                                                                                                         [ 50%]
pytestexample.py::test_silnia_0 PASSED                                                                                                                                              [ 75%]
pytestexample.py::test_silnia_string PASSED                                                                                                                                         [100%]

=================================================================================== 4 passed in 0.02s ====================================================================================


Podsumowanie

Trzy różne frameworki – dwa wbudowane, jeden zewnętrzny, wszystkie darmowe, wszystkie na swój sposób wyjątkowe i z pewnością użyteczne. Który z nich jest najlepszy? To już stety niestety kwestia indywidualna – zależna od projektu, umiejętności zespołu, własnych upodobań, celów i wielu innych czynników. W tym artykule pokazałem prostotę testów, niezależnie, który z frameworków wybierzemy.

Osobiście skłaniam się ku PyTestowi, gdyż daje najszersze możliwości, ale jeśli skupiamy się tylko i wyłącznie na poziomie testów jednostkowych, to zarówno unittest, jak i doctest w niczym mu nie ustępują. Decydującym czynnikiem może być tu preferencja składni, ale to – jak już wspomniałem – kwestia indywidualna.


O autorze

Autorem artykułu jest Grzegorz Janicki, Senior Test Engineer w TietoEvry. Od 2011 roku zawodowo zajmuje się testowaniem sprzętu i oprogramowania. Karierę rozpoczął jako Junior Test Engineer, przechodząc przez wszystkie szczeble awansu zawodowego, aż do pozycji Test Leada / Test Managera. Aktualnie współpracuje z TietoEvry jako Senior Test Engineer, a także lider zespołu zajmującego się wsparciem z zakresu testowania produktów oraz zapewnieniem narzędzi testowych dla zespołów deweloperskich.

Poza TietoEvry zajmuje się szkoleniami z zakresu ISTQB oraz programowania w języku Python. Prywatnie szczęśliwy mąż i ojciec 4-letniego urwisa, hobbystycznie podróżuje, majsterkuje i pasjonuje się tematyką DIY. 


O TietoEVRY

TietoEVRY jest wiodącą w Europie Północnej firmą informatyczną świadczącą usługi w zakresie budowy i rozwoju zaawansowanych systemów informatycznych z wykorzystaniem najnowszych technologii.

Początki firmy sięgają roku 1968. W ciągu kilkunastu lat z głównej siedziby w Helsinkach firma rozszerzyła swoją działalność otwierając jednostki w ponad 20 krajach świata. Polski oddział firmy TietoEVRY został otwarty w 2006 r. Wśród klientów firmy są zarówno przedstawiciele sektora prywatnego, jak i publicznego. Dzięki zatrudnianiu dziesiątek tysięcy ekspertów, TietoEVRY jest w stanie świadczyć swoje usługi na poziomie globalnym na wszystkich etapach rozwoju produktów.

TietoEVRY ma swoje oddziały w Szczecinie, Wrocławiu i Krakowie. Od kilku lat polski oddział TietoEVRY znajduje się w czołówce ekspertów oprogramowania.

Zobacz więcej na Bulldogjob