Moduł pytest

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

INSTALALACJA

'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.

WPROWADZENIE

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.

IMPORT PYTEST


# 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()

FIXTURES

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")

MARKERY

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

TESTY W KLASACH

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])