Sytuacja kobiet w IT w 2024 roku
22.06.20218 min
Mateusz Rechnio
Vewd Software Poland Sp. z o.o.

Mateusz RechnioQA EngineerVewd Software Poland Sp. z o.o.

Pytest vs Unittest - porównanie frameworków do automatyzacji testów w Pythonie

Zobacz porównanie frameworków Pytest i Unittest pod kątem struktury, konfiguracji, parametryzacji testów, zarządzanie testami oraz społeczności.

Pytest vs Unittest - porównanie frameworków do automatyzacji testów w Pythonie

Dlaczego akurat te dwa frameworki? Unittest i Pytest to najczęściej wykorzystywane frameworki do automatyzacji testów napisane w języku Python. 


Unittest jest wzorowany na frameworku JUnit (napisanym w Javie) i jest zawarty w bibliotece standardowej Pythona, co czyni go łatwo dostępnym oraz pewnym rozwiązaniem. Natomiast Pytest jest obecnie jednym z najbardziej popularnych open - sourcowych frameworków na rynku. Obydwa dostarczają kompletne rozwiązania w kontekście testowania oprogramowania, jak.: runnery testów, klasy reprezentujące zbiory testów, rozbudowane konfiguratory, możliwość parametryzacji testów.

Czym się zatem różnią? Jaki framework najlepiej wybrać do testów automatycznych dla złożonego, komercyjnego projektu? Który z nich będzie lepszym wyborem dla małego, prywatnego przedsięwzięcia? Postaram się odpowiedzieć na powyższe pytania poprzez porównanie wspomnianych frameworków z uwagi na następujące cechy:

  • Dostarczone struktury
  • Konfiguracja
  • Parametryzacja testów
  • Zarządzanie testami
  • Społeczność


Przejdźmy zatem do porównania.

Dostarczone struktury

Unittest

Dostarczone struktury

Unittest wymusza stałą strukturę testów. Zbiory testów, czyli zbiory zawierające testy i mające na celu dogłębne przetestowanie produktu - reprezentowane są poprzez klasy dziedziczące po klasie unittest.TestCase. Pojedyncze testy reprezentowane są poprzez metody klas będących zbiorami testów. Klasa unittest.TestCase dostarcza między innymi metody setUp, tearDown (przygotowujące środowisko przed i po teście) oraz metody setUpClass, tearDownClass (przygotowujące środowisko przed i po zbiorze testów).

Ponadto, jeśli użytkownik zaimplementuje funkcje setUpModule i tearDownModule, to zostaną one uruchomione kolejno po załadowaniu modułu z testami oraz przed jego zakończeniem.

import unittest
 
class ExampleTestSuite(unittest.TestCase):
    def setUp(self):  # Prepare test environment
        pass
 
    def test_addition(self):
        self.assertEqual(1 + 2, 3)
 
    def tearDown(self):  # Cleanup after test
        pass


Nazewnictwo

Unittest posiada funkcję autowykrywania testów w plikach, których nazwa zaczyna się od prefixu test. Dzięki tej funkcji użytkownik nie musi ręcznie podawać ścieżek do plików tekstowych. Zbiory testów mogą mieć dowolną nazwę, jeśli dziedziczą po klasie unittest.TestCase. Aby metody zostały rozpoznane jako testy, muszą zaczynać się prefiksem test, np. metoda o nazwie check_compatibility nie zostanie rozpoznana jako test, ale metoda test_compatibility już tak.

Rezultaty

Wynik pojedynczego testu determinowany jest przez sprawdzenie, czy podczas jego wykonania pojawił się nieobsłużony wyjątek:

  • Jeśli wyjątek to AssertionError, Unittest ustawia wynik testu na Fail
  • Jeśli wyjątek to unittest.SkipTest, Unittest ustawia wynik testu na Skipped
  • Jeśli wyjątek to nie AssertionError ani unittest.SkipTest, Unittest ustawia wynik testu na Error
  • Jeśli żaden wyjątek się nie pojawił, Unittest ustawia wynik testu na OK


Unittest dostarcza razem z klasą unittest.TestCase zbiór metod z prefixem assert, które służą do porównywania wyników (np.: assertTrue, assertEqual, assertRaises):

def test_addition(self):
    self.assertEqual(1 + 2, 3)  # Will pass
    self.assertEqual(1 + 2, 4)  # Will fail
 
def test_addition_2(self):
    raise RuntimeError("Something happened")  # Will raise error


Pytest

Dostarczone struktury

Pytest daje użytkownikowi więcej swobody. Testy reprezentowane są przez funkcje lub metody - w zależności od implementacji. Framework ten dzieli testy na “zakresy” (ang. scopes): sesja testowa, moduł testowy, klasa testowa, test. Sesja testowa jest, innymi słowy, zbiorem wszystkich modułów, które zostały rozpoznane jako moduły testowe. Moduł zaś to plik napisany w języku Python. Pytest nie wymusza definiowania zbiorów testów w formie klas, ale daje użytkownikowi taką możliwość:

import pytest
 
def test_addition():
    assert 1 + 2 == 3
 
class ExampleTestSuite:
    def test_addition(self):
        assert 1 + 2 == 3


Pytest dostarcza także funkcjonalność nazwaną fiksturą. Jest to funkcja, której celem jest przygotowanie środowiska testowego dla danego testu, zbioru testów, modułu testowego, czy całej sesji i/lub posprzątanie po ich zakończeniu. Dodatkowo obiekty zwracane przez fikstury mogą być wstrzykiwane do wybranych testów. Dzięki temu rozwiązaniu fikstury mogą także przygotowywać testowane obiekty, np.:

@pytest.fixture(scope="function")
def connector():
    conn = Connector("login", "passwd")
    yield conn
    del conn
 
def test_connection(connector):
    assert connector.send_msg("Hello")


W tym przykładzie fikstura connector tworzy obiekt klasy Connector, zatrzymuje swoje działanie i uruchamia test (poprzez wykorzystanie słowa yield). Referencja do obiektu zostaje wstrzyknięta do testu poprzez argument o tej samej nazwie, co fikstura. Po wykonaniu testu sterowanie wraca do fikstury, która usuwa obiekt klasy Connector. Dodatkowo Pytest dostarcza gotowe fikstury, które ułatwiają zarządzanie testami.

Nazewnictwo

Pytest rozpoznaje elementy kodu po prefiksie test. Jeśli dany plik, klasa, czy funkcja spełniają to założenie, to zostaną zebrane przez framework. Istnieje także możliwość zmiany warunków zbierania testów - więcej na ten temat w sekcji Konfiguracja.

Rezultaty

Definiowanie i reprezentacja wyników przez Pytest jest bardzo zbliżona do rozwiązań z frameworku Unittest. Jedyne różnice jakie znajdziecie, to:

  • wykorzystanie słowa kluczowego assert do porównywania wyników
  • dwa dodatkowe statusy wyników (czyli XFAIL i XPASS)
  • pominięcie testu nie oznacza pojawienia się żadnego wyjątku
  • drobne różnice w nazwach statusów

Konfiguracja

Unittest

Unittest nie dostarcza gotowych rozwiązań w kontekście konfiguracji. Wymagane jest użycie innych rozwiązań z biblioteki standardowej Pythona (np. argparse) lub rozwiązań 3rd party (np. click).

Pytest

Pytest akceptuje różne formaty plików konfiguracyjnych, m.in.: pytest.ini, pyproject.toml, tox.ini, setup.cfg, za pomocą których można zdefiniować np.: wersję testów, ścieżki do modułów testowych. Pytest dostarcza jeszcze jedną formę konfiguracji, a mianowicie plik conftest.py. Jeżeli plik ten istnieje w danym folderze zawierającym moduły testowe, jest on ładowany w trakcie zbierania testów.

Plik ten może modyfikować wiele funkcji: od dodawania dodatkowych flag poprzez zmianę sposobu zbierania testów (np. poprzez filtrowanie nazw za pomocą regexów) aż po modyfikację konkretnych etapów wykonywania procedury testowej. Możliwości wykorzystania pliku conftest.py jest za wiele, aby opisywać je tutaj - uważam, że zasługuje on na własny artykuł.

Parametryzacja

Unittest

Unittest nie dostarcza natywnej możliwości parametryzacji testów. Wymagane jest użycie innych rozwiązań 3rd party (np. parameterized).

Pytest

Pytest dostarcza moduł mark pozwalający na oznaczanie testów. Jednym z tych oznaczeń jest dekorator parametrize, którego celem jest wygenerowanie określonej ilości testów na podstawie parametrów podanych przez użytkownika, np.:

@pytest.mark.parametrize("a,b", [(1, 2), (3, 4)])
def test_addition(a, b):
    print(a + b)


W ten sposób pytest wygeneruje dwa testy, gdzie argumenty a i b będą parami liczb (1, 2), (3, 4).

$ python -m pytest -vvv
...
collected 2 items
 
test_param.py::test_addition[1-2] PASSED   [ 50%]
test_param.py::test_addition[3-4] PASSED   [100%]


Dekorator ten może także generować testy, tworząc kombinacje parametrów.

@pytest.mark.parametrize("a", [1, 2])
@pytest.mark.parametrize("b", [3, 4])
def test_addition(a, b):
    print(a + b)


W tym wypadku wygenerowane zostaną cztery testy, gdzie argumenty a i b będą parami liczb (3, 1), (3, 2), (4, 1), (4, 2).

$ python -m pytest -vvv
...
collected 4 items
 
test_param.py::test_addition[3-1] PASSED   [ 25%]
test_param.py::test_addition[3-2] PASSED   [ 50%]
test_param.py::test_addition[4-1] PASSED   [ 75%]
test_param.py::test_addition[4-2] PASSED   [100%]

Zarządzanie testami

Unittest

Dostarcza cztery dekoratory używane do pomijania testów: skip, skipIf, skipUnless, expectedFailure oraz jedną metodę: TestCase.skipTest. Dekoratory są wykorzystywane do kompletnego pominięcia pojedynczego testu lub klasy reprezentującej zbiór testów. Jeśli wspomniana metoda zostanie wywołana podczas wykonywania testu, to zostanie on przerwany i oznaczony jako pominięty.

    @unittest.skipIf(condition=sys.platform("win32", reason="Can't run on Windows"))
    def test_addition_2(self):
        self.assertEqual(1 + 2, 4)
 
    @unittest.expectedFailure
    def test_addition_3(self):
        raise RuntimeError("ERROR HAPPENED")


Pytest

Dostarcza trzy dekoratory do pomijania testów: mark.skip, mark.skipif, mark.xfail oraz trzy funkcje: importskip, skip, xfail. Dekoratory są wykorzystywane do kompletnego pominięcia pojedynczego testu lub klasy reprezentującej zbiór testów. Jeśli funkcja skip zostanie użyta podczas wykonywania testu, zostanie on przerwany. Funkcje skip oraz importskip mogą zostać użyte do pominięcia całego modułu testowego. Dekorator mark.xfail i funkcja xfail oznaczają test jako test, który powinien zakończyć się niepowodzeniem.

os = pytest.importorskip(modname="os")
 
@pytest.mark.skipif(condition=sys.platform("win32"), reason="Can't run on Windows")
def test_addition():
    self.assertEqual(1 + 2, 4)
 
@pytest.mark.xfail
def test_addition_3():
    raise RuntimeError("ERROR HAPPENED")


Pytest dostarcza także możliwość dodawania własnych oznaczeń oraz możliwość zarządzania nimi na różnych etapach pracy frameworku poprzez plik conftest.py.

Społeczność

W tej części chciałbym przeanalizować dane związane z popularnością porównywanych frameworków wśród społeczności testerów, np.: liczba zapytań na StackOverflow, ilość zgłoszonych problemów i forków na GitHubie.

Unittest

stackoverflow:

  • Szukanie po frazie unittest zwraca 19,522 wyniki. Nie jest to do końca miarodajna liczba, ponieważ fraza unittest może odnosić się do ogólnego pojęcia testów jednostkowych.
  • Szukanie po frazie [python] unittest zwraca 10,851 wyników.


github
(Niestety framework Unittest nie ma indywidualnego repozytorium, jest zawarty bezpośrednio w repozytorium Pythona):

  • Około 38,100 gwiazdek.
  • Około 18,900 forków.


Pytest

stackoverflow:

  • Szukanie po frazie pytest zwraca 17,401 wyników. Wynik ten nie jest przekłamany ze względu na oryginalną nazwę frameworku.
  • Szukanie po frazie [python] pytest zwraca 13,550 wyników.


github
:

  • Około 7400 gwiazdek.
  • Około 1700 forków.
  • 619 zgłoszonych problemów.


Dodatkowo, warto nadmienić fakt, iż Pytest wspiera pluginy, które tworzone są w większości przez społeczność. Pluginy te pozwalają na poszerzenie funkcjonalności, np.: plugin pytest-xdist pozwala na uruchamianie testów w równoległych procesach, plugin pytest-aws pozwala na testowanie rozwiązań związanych z Amazon Web Services. Na oficjalnej stronie frameworku Pytest została zamieszczona lista dostępnych pluginów, według której jest ich aktualnie 868.

Na podstawie przedstawionych danych ze stackoverflow można wywnioskować, iż obydwa frameworki są porównywalne pod względem wsparcia społeczności, dane z github nie są jednak w pełni obiektywne, ponieważ Unittest jest zawarty w bibliotece standardowej Pythona.

Podsumowanie

Obydwa frameworki są dojrzałymi i docenianymi narzędziami aktualnie dostępnymi na rynku. Część funkcji jest bardzo podobna, co sprawia, że migracja między nimi nie wymaga dużego nakładu pracy ze strony użytkownika. Jednakże mogę z czystym sumieniem stwierdzić, że Pytest jest bardziej kompletnym frameworkiem z lepszym wsparciem autorów oraz większą społecznością rozwijającą to narzędzie (poprzez pluginy, czy zgłaszanie błędów). Z drugiej strony, w porównaniu do frameworku Unittest, wymaga on od użytkownika lepszej znajomości Pythona, co czyni go trudniejszym do opanowania przez osoby początkujące.

Podsumowując, gdybym miał ocenić, który z nich jest lepszy - odpowiedziałbym, że wybór zależy od tego, jaki problem chcemy rozwiązać. Jeśli jesteś początkującym programistą, zacznij od frameworku Unittest. Jeżeli znasz język Python i potrzebujesz dojrzałego, skalowalnego rozwiązania, z którym będziesz pracował wraz z zespołem - wybierz Pytest.

W naszej firmie korzystamy z obu frameworków, jednakże do tworzenia nowych testów używamy obecnie wyłącznie Pytest. Ze względu na dużą ilość platform, na których testujemy nasze produkty najważniejszymi atutami były: parametryzacja testów, wsparcie konfiguracji środowiska testowego oraz zarządzanie testami.

Unittest wymagał od deweloperów i QA-ów większej ilości czasu poświęconego na utrzymanie i rozwój testów automatycznych ze względu na potrzebę implementacji wyżej wymienionych rozwiązań, bądź używanie dodatkowych bibliotek. Oczywiście pluginy wspomniane w tym artykule nie są tworzone przez autorów frameworku Pytest, ale ze względu na jego architekturę, ich działanie jest bardziej przewidywalne.

O autorze

Mateusz Rechnio pracuje jako QA Engineer w Vewd. Od ponad 3 lat będąc w branży IT stoi na straży jakości i bezawaryjności wytwarzanego oprogramowania. We wszystkich działaniach kieruje się zasadą logicznego myślenia, które jak twierdzi: "jest kluczem do sprawnego rozwiązywania problemów i tworzenia dobrze działającego software'u". W Vewd najbardziej ceni sobie pracę z doświadczonymi ludźmi w świetnej (“przekozak”) atmosferze.

<p>Loading...</p>