https://docs.pytest.org/
https://pypi.org/project/pytest/
https://www.python-course.eu/python3_pytest.php
https://bulldogjob.pl/readme/pytest-vs-unittest-porownanie-frameworkow-do-automatyzacji-testow-w-pythonie
'pytest' nie należy do biblioteki standardowej, więc trzeba go dodatkowo zainstalować.
PIP (konto użytkownika lub środowisko wirtualne) python3 -m pip install pytest APT Debian packages: python-pytest (Py2.7), python3-pytest (Py3), oraz zależności.
Framework 'pytest' (Py3.6+) ułatwia pisanie krótkich testów, ale również dobrze się skaluje i wspiera testowanie dużych aplikacji i bibliotek.
Pliki z testami powinny mieć odpowiednie nazwy (spam.py to moduł do testowania): test_spam.py lub spam_test.py.
Można przygotować środowisko do testów z 'pytest' za pomocą plików konfiguracyjnych w różnych formatach (pytest.ini, pyproject.toml, tox.ini, setup.cfg, conftest.py).
# Zawartość pliku test_sample.py def inc(x): return x + 1 def test_answer(): # wymagana nazwa test_* assert inc(3) == 5 # celowy błąd, używamy 'assert'
# Uruchamianie testów: $ pytest # discovery mode ============================= test session starts ============================== platform linux2 -- Python 2.7.16, pytest-3.10.1, py-1.7.0, pluggy-0.8.0 rootdir: .../week12/pytest1, inifile: collected 1 item test_sample.py F [100%] =================================== FAILURES =================================== _________________________________ test_answer __________________________________ def test_answer(): > assert inc(3) == 5 E assert 4 == 5 E + where 4 = inc(3) test_sample.py:8: AssertionError =========================== 1 failed in 0.02 seconds ===========================
# Testy po poprawieniu błłedów. $ pytest # discovery mode ============================= test session starts ============================== platform linux2 -- Python 2.7.16, pytest-3.10.1, py-1.7.0, pluggy-0.8.0 rootdir: .../week12/pytest1, inifile: collected 1 item test_sample.py . [100%] =========================== 1 passed in 0.01 seconds =========================== # In Debian 10, 'python3 -m pytest' will use Python 3.7.
Raport zwracany przez 'pytest' pokazuje:
(1) stan systemu,
(2) katalog do przeszukiwania i pliki z testami,
(3) liczbę wykrytych testów (funkcji testujących).
Na wyjściu dostajemy status każdego testu w postaci podobnej do 'unittest':
(1) kropka (.) oznacza sukces testu,
(2) litera 'F' oznacza porażkę testu,
(3) litera 'E' oznacza pojawienie się niespodziewanego wyjątku podczas testu.
# Zawartość pliku test_set.py import pytest def test_union(): setA = set([1, 3, 5]) setB = set([2, 3]) assert setA | setB == set([1, 2, 3, 5]) assert setA.union(setB) == set([1, 2, 3, 5]) def test_intersection(): setA = set([1, 3, 5]) setB = set([2, 3]) assert setA & setB == set([3]) assert setA.intersection(setB) == set([3]) def test_difference(): setA = set([1, 3, 5]) setB = set([2, 3]) assert setA - setB == set([1, 5]) assert setA.difference(setB) == set([1, 5]) if __name__ == "__main__": pytest.main()
# Uruchomienie testów: $ python test_set.py # używa pytest.main()
Można myśleć o testach, że składają się z czterech kroków:
(1) Arrange - przygotowanie wszystkiego do testów.
(2) Act - uruchomienie działań, które chcemy testować.
(3) Assert - sprawdzenie wyników.
(4) Cleanup - czyszczenie po testach.
'Fixtures' to działania dla kroku 'Arrange'.
@pytest.fixture # dekorator def setA(): return set([1, 3, 5]) @pytest.fixture def setB(): return set([2, 3]) def test_union(setA, setB): assert setA | setB == set([1, 2, 3, 5]) assert setA.union(setB) == set([1, 2, 3, 5]) def test_intersection(setA, setB): assert setA & setB == set([3]) assert setA.intersection(setB) == set([3]) def test_difference(setA, setB): assert setA - setB == set([1, 5]) assert setA.difference(setB) == set([1, 5])
# Testowanie występowania wyjątków. def test_exceptions(): pytest.raises(TypeError, len, 10) pytest.raises(TypeError, abs, "word") pytest.raises(ZeroDivisionError, lambda: 1/0) pytest.raises(IndexError, lambda: list()[1]) pytest.raises(KeyError, lambda: dict()["key"])
# Do tworzenia testów dotyczących wyzwalanych wyjątków # można użyć pytest.raises() jako menedżera kontekstu. def test_zero_division(): with pytest.raises(ZeroDivisionError): 1 / 0 def test_recursion_depth(): with pytest.raises(RuntimeError) as excinfo: def f(): f() f() assert "maximum recursion" in str(excinfo.value) # excinfo to instancja 'ExceptionInfo', jest to wrapper dla bieżącego wyjątku. # Najbardziej interesujące atrybuty to # excinfo.type, excinfo.value, and excinfo.traceback.
# Jednoczesne testowanie klasy wyjątku i komunikatu z użyciem 'match'. def test_recursion_depth(): with pytest.raises(RuntimeError, match="maximum recursion"): def f(): f() f() def test_values(): with pytest.raises(ValueError, match='must be 0 or None'): raise ValueError('value must be 0 or None') with pytest.raises(ValueError, match=r'must be \d+$'): # regular expression raise ValueError('value must be 42')
# Użycie czynności przygotowawczych i czyszczących. @pytest.fixture(scope="function") def connector(): conn = Connector("login", "passwd") yield conn # przejście do testu del conn # czynności czyszczące def test_connection(connector): assert connector.send_msg("Hello")
Funkcje testujące mogą być udekorowane przez 'pytest.mark'. Można również tworzyć własne markery, które muszą być rejestrowane.
$ pytest --markers # lista istniejących markerów @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/latest/warnings.html#pytest-mark-filterwarnings @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test. ...
# Przykładowe markery. @pytest.mark.skip("no way of currently testing this") @pytest.mark.skipif('sys.platform == "win32"') @pytest.mark.xfail() @pytest.mark.xfail(raises=IndexError) # powód porażki testu @pytest.mark.parametrize(argnames, argvalues) @pytest.mark.tryfirst @pytest.mark.trylast
# Zawartość pliku fib2.py def fibonacci(n): old, new = 0, 1 for _ in range(n): old, new = new, old + new return old
# Zawartość pliku test_fib2.py import pytest from fib2 import fibonacci @pytest.mark.parametrize('n, res', [(0, 0), (1, 1), (2, 1), (3, 2), (4, 3), (5, 5), (6, 8)]) def test_fibonacci(n, res): assert fibonacci(n) == res
Grupowanie testów w klasach może być pożyteczne z kilku powodów:
(1) lepsza organizacja testów,
(2) dzielenie fixtures dla testów tylko w danej klasie,
(3) stosowanie markerów na poziomie klas i domyślne ich stosowanie
dla wszystkich testów w klasie.
Każdy test otrzymuje osobną instancję klasy.
import pytest class TestSets: # 'Test' prefix jest ważny # scope: zakres dzielony przez daną 'fixture'; # "function" (default), "class", "module", "package", "session" @pytest.fixture(scope="class") def setA(self): # 'self' is the first argument in methods return set([1, 3, 5]) @pytest.fixture(scope="class") def setB(self): return set([2, 3]) def test_union(self, setA, setB): assert setA | setB == set([1, 2, 3, 5]) assert setA.union(setB) == set([1, 2, 3, 5]) def test_intersection(self, setA, setB): assert setA & setB == set([3]) assert setA.intersection(setB) == set([3]) def test_difference(self, setA, setB): assert setA - setB == set([1, 5]) assert setA.difference(setB) == set([1, 5])