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
import pytest
def inc(x):
return x + 1
def test_answer(): # wymagana nazwa test_*
# W testach nie używamy docstringów.
# Chcemy zachować standardowe komunikaty o błędach.
assert inc(3) == 5 # celowy błąd, używamy 'assert'
assert pytest.approx(inc(10.1), abs=0.1) == 11.0
assert pytest.approx(inc(10.1), rel=0.01) == 11.0 # default rel=1e-6
assert pytest.approx(inc(1./3.)) == 1.333333 # default rel=1e-6
# 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.
# pytest can run unittest tests (from test*.py files) and doctest tests # (from test*.txt files). $ python3 -m pytest --doctest-glob="*.rst" # *.rst files with doctest tests
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])
# UWAGA To nie jest równoważne podstawieniu
# setA = set([1, 3, 5])
# Sprawdzenie fixtures zdefiniowanych dla test cases: pytest --fixtures
@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 # pojedyńcza instrukcja wyzwalająca wyjątek
def test_recursion_depth():
with pytest.raises(RuntimeError) as excinfo:
def f():
f()
f() # pojedyńcza instrukcja wyzwalająca wyjątek
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() # pojedyńcza instrukcja wyzwalająca wyjątek
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 (poniżej yield)
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])