Pytest – czemu cieszy się większą popularnością niż unittest?

W swojej karierze zawodowej pisałem testy w obu frameworkach: zarówno unittest, jak i pytest. Oba rozwiązania to świetne narzędzia z różnymi wadami i zaletami, ale pytest obecnie jest zdecydowanie popularniejszy. W tym krótkim artykule podzielę się z Wami kilkoma funkcjami pytesta, które moim zdaniem jasno odpowiadają na tytułowe pytanie.

Doświadczenie z unittest gotowe do wykorzystania w pytest

Po pierwsze warto nadmienić, że klasa TestCase z unittest jest obsługiwana przez pytest od samego początku. Wszystkie zachowania wymyślone podczas pisania testów w unittest (np. pisanie asercji) pozostają bez zmian, co niezmiernie ułatwia zmianę frameworku. W dodatku, większość funkcji unittest również jest obsługiwana, wobec czego wykonawcy pytest mogą też przeprowadzać stare testy (podtesty nie są obsługiwane).

Co więcej, niektóre funkcje pytest działają w ramach podklas unittest.TestCase, jak np. oznaczania testów.

Uruchamianie istniejącego testu opartego na unittest jest tożsame z wykonaniem następującego polecenia:

$ pytest my_unittest_tests

Poniżej demonstracja tego, jak zmieszane funkcje unittest i pytest współdziałają ze sobą:

import pytest
import unittest


class ClassTest(unittest.TestCase):

     @pytest.mark.xfail
     def test_feature_a(self):
        self.assertEqual(2, 3)

     def test_feature_b(self):
        self.assertTrue(True)

Zakładając, że kod ten jest zapisany w run_class.py, komenda wykonująca wygląda następująco:

$ pytest run_class.py

Wynik:

$ pytest run.py
============================= test session starts ==========================
platform win32 -- Python 3.6.3, pytest-4.0.2, py-1.7.0, pluggy-0.8.0
rootdir: path_to_root\examples, inifile:
collected 2 items
run.py x.
[100%]
===================== 1 passed, 1 xfailed in 0.07 seconds =====================

Wykonywanie testów rozproszonych na wielu procesorach przy pomocy xdist

Pytest daje nam możliwość wykonywania równoległych testów na wielu procesach na raz za pomocą pytest-xdist. Im więcej testów, tym większa korzyść (co najmniej dwukrotnie zwiększona szybkość).

W pierwszej kolejności trzeba zainstalować moduł, ponieważ nie jest on domyślny dla pytest:

$ pip install pytest-xdist

Aby uruchomić test konkretnego modułu na wielu procesach, wykonaj poniższe polecenie (n odpowiada liczbie równoległych procesów):

$ pytest test_class.py -n 6

W kontekście czasu wykonywania testów równoległych należy zwrócić uwagę na jedną kluczową kwestię. Każdy podproces zdefiniowany przez argument „-n” tworzy swój własny zestaw z własną konfiguracją wydzieloną dla danej sesji (części wspólne wykonywanych poleceń między testami oraz rozwiązanie conftest.py). Dlatego jeśli testy rozpoczynają zestaw od kilku ciężkich operacji ładujących, czas wykonania tego kroku zostanie pomnożony przez wartość „-n”, co w rzeczywistości może obniżyć wartość potencjalnych korzyści w zakresie czasu wykonania.

Bezproblemowa integracja z modułem parametryzowanym

Pytest doceniany jest za świetną integrację z wieloma modułami. Skoro już o tym mowa, pytest daje natychmiastowy dostęp do testów parametryzowanych, ale zdecydowałem się skorzystać z modułu pochodzącego z innego źródła (ze względu na ograniczenia testów parametryzowanych przy stosowaniu podklasy unittest.TestCase).

Moduł parametryzowany zainstaluj za pomocą następującego polecenia:

$ pip install parameterized

Ten moduł umożliwia wykonanie danego testu n razy przy użyciu różnych zbiorów danych – wystarczy zastosować dekorator w metodzie testowej.

import requests
import unittest
from parameterized import parameterized


class TestRepositories(unittest.TestCase):

    @parameterized.expand(['autorepr', 'django-nose', 'chai-subset'])
    def test_repos_existence_in_wolever_profile(self, repo):
        api_url = 'https://api.github.com/users/wolever/repos'
        response = requests.get().json(api_url)
        self.assertIn(repo, [item['name'] for item in response])

Pytest automatycznie tworzy trzy metody testowe z przyrostkiem „repo”. Do wykonania testów paremetryzowanych nie potrzebujemy dodatkowych parametrów. Jasną zaletą jest ponowne wykorzystanie istniejącego kodu na potrzeby wielu testów.

Wynik będzie wyglądał tak:

test_repos.py::TestRepositories::test_repos_existence_in_wolever_profile_0_autorepr 
test_repos.py::TestRepositories::test_repos_existence_in_wolever_profile_1_django_nose 
test_repos.py::TestRepositories::test_repos_existence_in_wolever_profile_2_chai_subset

Oznaczanie testów jako sposób organizowania zestawów testów

Główny sposób organizowania zestawów testów polega na trzymaniu ich w modułach, np. client_registration, shopping, transaction itp., ale co jeśli mamy potrzebę zbudowania zestawu testów krzyżowych (zebrać testy z kilku modułów)? Pytest wskazuje jedno z rozwiązań, które uważam za elastyczne i czytelne – oznaczenia.

Załóżmy, że z naszej aplikacji korzysta kilku klientów, którzy wdrożyli różne zestawy metod płatności. Za pomocą funkcji oznaczania możemy jasno określić, które testy wiążą się z danym klientem.

Przykładowo, załóżmy, że mamy dwa pliki pythonowe: test_auth.py i test_payments.pytest_auth.py.

import pytest

@pytest.mark.client_a
def test_authentication_of_client_a():
    pass

@pytest.mark_client_b
def test_authentication_of_client_b():
    pass

test_payments.py

import pytest


@pytest.mark.client_a
def test_paypal_payment():
    pass


@pytest.mark.client_a
@pytest.mark.client_b
def test_credit_card_payment():
    pass


@pytest.mark.client_b
def test_apple_pay_payment():
    pass

Aby uruchomić tylko testy klienta client_A lub client_B, wystarczy użyć poniższego polecenia:

$ pytest –m client_a

Pytest wyszukuje i wybiera testy zgodne z danym oznaczeniem, dając nam wynik:

============================= test session starts =============================
platform win32 -- Python 3.6.3, pytest-4.0.2, py-1.7.0, pluggy-0.8.0
rootdir: C:\Repozytorium, inifile:
plugins: xdist-1.25.0, forked-0.2
collected 5 items / 2 deselected
 
examples\test_auth.py .                                                  [ 33%]
examples\test_payments.py ..                                             [100%]
 
=================== 3 passed, 2 deselected in 0.20 seconds ====================

Przydatne rozszerzenie – flake8

Kolejną zaletę pytest stanowią liczne rozszerzenia, które integrują powszechnie używane narzędzia z frameworkiem pytest.

Rozszerzenie można zainstalować za pomocą tego polecenia:

$ pip install pytest-flake8

Następnie będziemy mogli wywołać sprawdzenie flake8 w bieżącej lokalizacji za pomocą polecenia:

$ pytest --flake8

Zwróć uwagę, że instalacja pytest-flake8 nie przejmuje obsługiwania Twojej wersji elementów podporządkowanych flake8. W każdym razie, jeśli dojdzie do takiego konfliktu, otrzymamy powiadomienie, np.:

pluggy.manager.PluginValidationError: Plugin 'flake8' could not be loaded: (setuptools 28.8.0 (c:\users\username\appdata\local\programs\python\python36\lib\site-packages), Requirement.parse('setuptools>=30'), {'flake8'})!

Powyższy wycinek pokazuje, że biblioteka setuptools musi zostać zaktualizowana do wersji 30 lub nowszej.

Ponieważ uruchomienie domyślnego flake checka w projekcie prawdopodobnie poskutkuje setkami komunikatów, dzięki opcji konfiguracji sprawdzania flake. Istotne jest to, że nie znajduje się ona w .flake8 jak to zwykle bywa dla flake, lecz w pliku setup.cfg lub tox.ini w sekcji [pytest].

# content of setup.cfg
[pytest]
flake8-max-line-length = 120
flake8-ignore = E201 E231
flake8-ignore =    *.py E201config/app_cfg.py ALL

Po rozpoznaniu błędu i poprawieniu ustawień flake-check, uda nam się zyskać pożądany wynik:

$ pytest example/config.py --flake8
============================= test session starts =============================
platform win32 -- Python 3.6.3, pytest-4.0.2, py-1.7.0, pluggy-0.8.0
rootdir: C:\Repozytorium, inifile:
plugins: xdist-1.25.0, forked-0.2, flake8-1.0.2
collected 1 item
 
example\config.py .                                                      [100%]
 
========================== 1 passed in 0.12 seconds ===========================

Muszę jednak Was ostrzec: ustawienie go jako hook dla git commit może być bardzo irytujące. Na przykład jeśli mamy jakiś pożar na produkcji, i nie możemy wykonać commita, by zatwierdzić poprawkę, bo walczymy z flake8. W innych przypadkach jednak zachowanie takich samych standardów kodu w całym projekcie jest bardzo przydatne.

Generowanie wyników testów HTML

Innym świetnym rozszerzeniem dla pytest, dającym możliwość raportowania w HTML, jest pytest-html. Zainstalujesz je za pomocą poniższego polecenia:

$ pip install pytest-html

Potem możesz przeprowadzać testy za pomocą:

$ pytest –-html=report.html

Wówczas wszystkie testy z bieżącego katalogu zostają zebrane i wykonane, a ich wyniki zapisywane w raporcie HTML również dostępnym w bieżącym katalogu, zgodnie z poleceniem.

Podsumowanie

Osobiście wolę korzystać z pytest niż unittest. Jest szybki i niezawodny. Mimo tego, że redukuje szablonowy kod do minimum, nadal można go odczytać. Choć nie można go znaleźć w standardowych bibliotekach (co dla niektórych może stanowić wadę), korzystanie z niego ma też kluczową zaletę, ponieważ nowe wersje pytesta nie są powiązane z oficjalnymi (i rzadszymi) aktualizacjami Pythona. Pytest pomaga mi organizować zestawy testów i pokazywać wyniki testów  Zdecydowanie polecam to narzędzie.

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami