pyTest Tutorial #9: Klasy z testami

W dziewiątej części serii pyTest Tutorial jak poprawnie stworzyć klasy z testami, a także jak je używać na podstawie przykładów.

Pytest Tutorial

Pytest Tutorial


To już dziewiąta część serii pyTest Tutorial. Tym razem spróbujemy stworzyć klasy z testami na podstawie skryptów testowych przygotowanych w poprzednich odsłonach. Wcześniej jednak, w formie przypomnienia, wprowadźmy zmiany w testach przygotowanych w części piątej:

Przeróbmy nasze testy dodawania, odejmowania, mnożenia oraz dzielenia na cztery sparametryzowane testy, z czego test_addition oraz test_subtraction będą używały parametrów add_sub_data, natomiast test_division oraz test_multiplication z div_mult_data. Polecam wykonanie tego zadania samemu, a następnie porównanie z wynikami poniżej.

from base import BasicCalculator
import pytest

add_sub_data = [(10, 5),
                (2, -4),
                (-10, 12)
                ]


@pytest.mark.parametrize("num1, num2", add_sub_data)
def test_addition(num1, num2):
    object = BasicCalculator()
    object.provide_number(num1)
    object.provide_operand('+')
    object.provide_number(num2)
    assert object.show_result()[0] == num1 + num2


@pytest.mark.parametrize("num1, num2", add_sub_data)
def test_subtraction(num1, num2):
    object = BasicCalculator()
    object.provide_number(num1)
    object.provide_operand('-')
    object.provide_number(num2)
    assert object.show_result()[0] == num1 - num2


div_mult_data = [(2, 2),
                 (-7, 3)
                 ]


@pytest.mark.parametrize("num1, num2", div_mult_data)
def test_multiplication(num1, num2):
    object = BasicCalculator()
    object.provide_number(num1)
    object.provide_operand('*')
    object.provide_number(num2)
    assert object.show_result()[0] == num1 * num2


@pytest.mark.parametrize("num1, num2", div_mult_data)
def test_division(num1, num2):
    object = BasicCalculator()
    object.provide_number(num1)
    object.provide_operand('/')
    object.provide_number(num2)
    assert object.show_result()[0] == num1 / num2

Po próbie uruchomienia powinniśmy otrzymać:

(env) qabrio@test:~/basic_calculator$ pytest -v test_calc.py
================================================ test session starts ================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- /home/qabrio/basic_calculator/env/bin/python3.6
cachedir: .cache
rootdir: /home/qabrio/basic_calculator, inifile:
collected 10 items                                                                                                  
 
test_calc.py::test_addition[10-5] PASSED                                                                      [ 10%]
test_calc.py::test_addition[2--4] PASSED                                                                      [ 20%]
test_calc.py::test_addition[-10-12] PASSED                                                                    [ 30%]
test_calc.py::test_subtraction[10-5] PASSED                                                                   [ 40%]
test_calc.py::test_subtraction[2--4] PASSED                                                                   [ 50%]
test_calc.py::test_subtraction[-10-12] PASSED                                                                 [ 60%]
test_calc.py::test_multiplication[2-2] PASSED                                                                 [ 70%]
test_calc.py::test_multiplication[-7-3] PASSED                                                                [ 80%]
test_calc.py::test_division[2-2] PASSED                                                                       [ 90%]
test_calc.py::test_division[-7-3] PASSED                                                                      [100%]
 
============================================= 10 passed in 0.01 seconds =============================================
(env) qabrio@test:~/basic_calculator$ 

Reguły tworzenia klas z testami

Zanim przejdziemy do stworzenia pierwszej klasy z wykorzystaniem powyższego kodu, najpierw kilka słów dotyczących samych klas testów. Więc klasa testów to nic innego jak specjalna klasa, której nazwa pasuje do wzorca rozpoznawalnego przez pyTest’a, czyli zaczyna się od Test, a następnie jest podany dowolny ciąg znaków. Poprawne więc będą nazwy takie jak TestSmoke, Testaddition, Test_add czy nawet Test1. Do niepoprawnych natomiast zaliczymy testAdd czy Addition. Najczęściej jednak używanym stylem jest forma TestAddition. Kolejną istotną rzeczą jest fakt, że klasa taka nie może posiadać jawnego konstruktora __init__. Trzecia ważna regułą traktuje, że w dalszym ciągu obowiązuje styl nazewnictwa dla skryptów testujących, czyli nazwy metod testująych w klasie muszą pasować do wzorca test_*.

Tworzenie klasy z testami

Wykorzystując klasy, możemy odpowiednio pogrupować testy, dzięki czemu operowanie nimi jest łatwiejsze. Spróbujmy zgodnie z powyższym przekonwertować nasze testy na dwie klasy TestAaddSub oraz TestDivMult:

from base import BasicCalculator
import pytest


class TestAddSub:
    add_sub_data = [(10, 5),
                    (2, -4),
                    (-10, 12)
                    ]

    @pytest.mark.parametrize("num1, num2", add_sub_data)
    def test_addition(self, num1, num2):
        object = BasicCalculator()
        object.provide_number(num1)
        object.provide_operand('+')
        object.provide_number(num2)
        assert object.show_result()[0] == num1 + num2

    @pytest.mark.parametrize("num1, num2", add_sub_data)
    def test_subtraction(self, num1, num2):
        object = BasicCalculator()
        object.provide_number(num1)
        object.provide_operand('-')
        object.provide_number(num2)
        assert object.show_result()[0] == num1 - num2


class TestDivMult:
    div_mult_data = [(2, 2),
                     (-7, 3)
                     ]

    @pytest.mark.parametrize("num1, num2", div_mult_data)
    def test_multiplication(self, num1, num2):
        object = BasicCalculator()
        object.provide_number(num1)
        object.provide_operand('*')
        object.provide_number(num2)
        assert object.show_result()[0] == num1 * num2

    @pytest.mark.parametrize("num1, num2", div_mult_data)
    def test_division(self, num1, num2):
        object = BasicCalculator()
        object.provide_number(num1)
        object.provide_operand('/')
        object.provide_number(num2)
        assert object.show_result()[0] == num1 / num2

Uruchamianie klasy z testami

Uruchamianie testów pogrupowanych w klasy jest analogiczne do uruchamiania zwykłych testów. Aby uruchomić wszystkie testy z pliku, wykorzystujemy standardową składnię.

qabrio@test:~/basic_calculator$ pytest -v test_calc.py
================================================ test session starts ================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- /home/qabrio/basic_calculator/env/bin/python3.6
cachedir: .cache
rootdir: /home/qabrio/basic_calculator, inifile:
collected 10 items                                                                                                  
 
test_calc.py::TestAddSub::test_addition[10-5] PASSED                                                          [ 10%]
test_calc.py::TestAddSub::test_addition[2--4] PASSED                                                          [ 20%]
test_calc.py::TestAddSub::test_addition[-10-12] PASSED                                                        [ 30%]
test_calc.py::TestAddSub::test_subtraction[10-5] PASSED                                                       [ 40%]
test_calc.py::TestAddSub::test_subtraction[2--4] PASSED                                                       [ 50%]
test_calc.py::TestAddSub::test_subtraction[-10-12] PASSED                                                     [ 60%]
test_calc.py::TestDivMult::test_multiplication[2-2] PASSED                                                    [ 70%]
test_calc.py::TestDivMult::test_multiplication[-7-3] PASSED                                                   [ 80%]
test_calc.py::TestDivMult::test_division[2-2] PASSED                                                          [ 90%]
test_calc.py::TestDivMult::test_division[-7-3] PASSED                                                         [100%]
 
============================================= 10 passed in 0.02 seconds =============================================
qabrio@test:~/basic_calculator$ 

Warto zwrócić uwagę, że obecnie mamy trzy sekcje oddzielone ::, w każdym teście, czyli [nazwa pliku]::[nazwa klasy]::[nazwa testu]. Na podstawie powyższego można wywnioskować, że uruchomienie klasy wykonujemy poprzez:

qabrio@test:~/basic_calculator$ pytest -v test_calc.py::TestDivMult
================================================ test session starts ================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- /home/qabrio/basic_calculator/env/bin/python3.6
cachedir: .cache
rootdir: /home/qabrio/basic_calculator, inifile:
collected 4 items                                                                                                   
 
test_calc.py::TestDivMult::test_multiplication[2-2] PASSED                                                    [ 25%]
test_calc.py::TestDivMult::test_multiplication[-7-3] PASSED                                                   [ 50%]
test_calc.py::TestDivMult::test_division[2-2] PASSED                                                          [ 75%]
test_calc.py::TestDivMult::test_division[-7-3] PASSED                                                         [100%]
 
============================================= 4 passed in 0.01 seconds ==============================================
qabrio@test:~/basic_calculator$ 

Natomiast w celu uruchomienia pojedynczego testu należy dodać kolejny podwójny dwukropek oraz jego nazwę:

qabrio@test:~/basic_calculator$ pytest -v test_calc.py::TestDivMult::test_division
================================================ test session starts ================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- /home/qabrio/basic_calculator/env/bin/python3.6
cachedir: .cache
rootdir: /home/qabrio/basic_calculator, inifile:
collected 2 items                                                                                                   
 
test_calc.py::TestDivMult::test_division[2-2] PASSED                                                          [ 50%]
test_calc.py::TestDivMult::test_division[-7-3] PASSED                                                         [100%]
 
============================================= 2 passed in 0.01 seconds ==============================================
qabrio@test:~/basic_calculator$ 

Markery dla klas

W jednej z poprzednich odsłon poznaliśmy markery, które używaliśmy w odniesieniu do konkretnych testów:

Warto zwrócić tutaj uwagę, że z powodzeniem można je stosować w odniesieniu do klas. Musimy sobie jednak zdawać sprawę, że zastosowanie markera dla klasy będzie równoznaczne z użyciem go dla każdego testu z osobna. Najlepiej to widać na przykładach. Dodajmy więc dla klasy TestAddSub marker xfail tak jak poniżej:

    @pytest.mark.xfail
    class TestAddSub:
    (...)

Po uruchomieniu kodu zobaczymy, że marker xfail został automatycznie zastosowany do wszystkich testów znajdujących się wewnątrz klasy:

(env) qabrio@test:~/basic_calculator$ pytest -v test_calc.py 
================================================ test session starts ================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- /home/qabrio/basic_calculator/env/bin/python3.6
cachedir: .pytest_cache
rootdir: /home/qabrio/basic_calculator
collected 10 items                                                                                                  
 
test_calc_7.py::TestAddSub::test_addition[10-5] XPASS                                                         [ 10%]
test_calc_7.py::TestAddSub::test_addition[2--4] XPASS                                                         [ 20%]
test_calc_7.py::TestAddSub::test_addition[-10-12] XPASS                                                       [ 30%]
test_calc_7.py::TestAddSub::test_subtraction[10-5] XPASS                                                      [ 40%]
test_calc_7.py::TestAddSub::test_subtraction[2--4] XPASS                                                      [ 50%]
test_calc_7.py::TestAddSub::test_subtraction[-10-12] XPASS                                                    [ 60%]
test_calc_7.py::TestDivMult::test_multiplication[2-2] PASSED                                                  [ 70%]
test_calc_7.py::TestDivMult::test_multiplication[-7-3] PASSED                                                 [ 80%]
test_calc_7.py::TestDivMult::test_division[2-2] PASSED                                                        [ 90%]
test_calc_7.py::TestDivMult::test_division[-7-3] PASSED                                                       [100%]
 
=========================================== 4 passed, 6 xpassed in 0.02s ============================================
(env) qabrio@test:~/basic_calculator$ 

Warto zwrócić uwagę na fakt, że markery znajdujące się przy klasie są analizowane w pierwszej kolejności. Trzeba zatem pamiętać o tej hierarchii, gdyż jeśli opatrzymy klasę markerem skip, a wybrane testy w klasie markerem xfail, marker skip zostanie zastosowany do wszystkich testów w klasie, a do markera xfail nigdy nie dotrzemy.

Parametry dla klas

Kolejną ważną funkcjonalnością są parametry oraz możliwość stosowania ich w odniesieniu do klas. Należy jednak pamiętać, że próba zdublowania parametryzacji skończy się wyjątkiem:

E   ValueError: duplicate 'num1'

Można jednak z powodzeniem wykonać parametryzację wielopoziomową. Zobaczmy więc, jak to wygląda na przykładzie. W tym celu zmieńmy istniejącą parametryzację dla testów test_addition na:

@pytest.mark.parametrize("num1", [3, 4])

Podobnie, dla testu test_subtraction, zmieńmy parametryzację na:

@pytest.mark.parametrize("num1", [7, 8])

Z kolei do klasy dodajmy parametry zgodnie z poniższym:

@pytest.mark.parametrize("num2", [1, 2])

Teraz możemy uruchomić tak przygotowane testy, w celu upewnienia się, że zostaną wykonane zgodnie z założeniami, przyjmując jeden parametr z poziomu klasy, natomiast pozostałe z bezpośredniej parametryzacji:

(env) qabrio@test:~/basic_calculator$ pytest -v test_calc_7.py::TestAddSub
================================================ test session starts ================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- /home/qabrio/basic_calculator/env/bin/python3.6
cachedir: .pytest_cache
rootdir: /home/qabrio/basic_calculator
collected 8 items                                                                                                   
 
test_calc_7.py::TestAddSub::test_addition[3-1] PASSED                                                         [ 12%]
test_calc_7.py::TestAddSub::test_addition[3-2] PASSED                                                         [ 25%]
test_calc_7.py::TestAddSub::test_addition[4-1] PASSED                                                         [ 37%]
test_calc_7.py::TestAddSub::test_addition[4-2] PASSED                                                         [ 50%]
test_calc_7.py::TestAddSub::test_subtraction[7-1] PASSED                                                      [ 62%]
test_calc_7.py::TestAddSub::test_subtraction[7-2] PASSED                                                      [ 75%]
test_calc_7.py::TestAddSub::test_subtraction[8-1] PASSED                                                      [ 87%]
test_calc_7.py::TestAddSub::test_subtraction[8-2] PASSED                                                      [100%]
 
================================================= 8 passed in 0.01s =================================================
(env) qabrio@test:~/basic_calculator$ 

W tym artykule poznaliśmy podstawowe reguły związane z tworzeniem klas testów, a także stworzyliśmy klasy modyfikując pliki z poprzednich odsłon tego tutorial’a pyTest’a. Przydatną umiejętności w tworzeniu grup testów jest korzystanie z markerów i parametryzacji dla klas. W kolejnej części poznamy możliwości związane z wykonywaniem setup’u testów, a także sposobach na posprzątanie po wykonaniu testów.

close

Newsletter