Python Testing With Pytest

2021.10.14

What is Pytest

Pytest is a feature-rich, plugin-based ecosystem for testing your Python code. The pytest framework helps to write simple and scalable test cases for databases, APIs, or UI. It support complex functional testing for applications and libraries. It helps to write tests from simple unit tests to complex functional tests. This blog will help you to understand basics of pytest and some of the tools pytest provides to keep your testing efficient and effective.

Features

  • Open source
  • Simple and Easy syntax
  • Skip tests
  • Detailed info on failing assert statements.
  • Can run a specific test or a subset of tests
  • Automatically detect tests and functions
  • Can run tests in parallel.
  • Python 3.6+ and PyPy 3
  • Modular fixtures
  • Can run unittest (including trial)

Install Pytest

Run the following command in your command line

pip install -U pytest

Check the installed version of pytest

pytest --version

Basics of pytest

Lets learn the basic of Pytest with following examble. Create the test file pytest_test.py with the below code.

pytest_test.py

import pytest 
def func(x):
    return x + 1

def test_1():
    assert func(3) == 4

def test_2():
    assert func(4) == 4

Execute the test

python3 -m pytest pytest_test.py

Result

platform darwin -- Python 3.8.2, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: XXXX
collected 2 items                                                                                                                  

pytest_test.py .F                                                                                                            [100%]

============================================================= FAILURES =============================================================
______________________________________________________________ test_2 ______________________________________________________________

    def test_2():
>       assert func(4) == 4
E       assert 5 == 4
E        +  where 5 = func(4)

pytest_test.py:16: AssertionError
===================================================== short test summary info ======================================================
FAILED pytest_test.py::test_2 - assert 5 == 4
=================================================== 1 failed, 1 passed in 0.05s ====================================================

Here in pytest_test.py .

Dot(.) says success. F says failure

The [100%] refers to the overall progress of running all test cases. After it finishes, pytest then shows a failure report because func(4) does not return 4.

Run a subset of entire test

Specific test or a subset of tests can be run by Pytest. Create the test file pytest_test.py with the below code.

pytest_test.py

import pytest
@pytest.mark.set1
def test_method1():
    x=5
    y=6
    assert x+1 == y,"test failed"


@pytest.mark.set2
def test_method2():
    x=5
    y=6
    assert x+1 == y,"test failed"

Execute the test

python3 -m pytest -m set1 pytest_test.py

Result

======================================================= test session starts ========================================================
platform darwin -- Python 3.8.2, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: XXXX
collected 2 items / 1 deselected / 1 selected                                                                                      

pytest_test.py .                                                                                                             [100%]

========================================================= warnings summary =========================================================
pytest_test.py:34
  XXXX/pytest_test.py:34: PytestUnknownMarkWarning: Unknown pytest.mark.set1 - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/mark.html
    @pytest.mark.set1

pytest_test.py:41
  XXXX/pytest_test.py:41: PytestUnknownMarkWarning: Unknown pytest.mark.set2 - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/mark.html
    @pytest.mark.set2

-- Docs: https://docs.pytest.org/en/stable/warnings.html
=========================================== 1 passed, 1 deselected, 2 warnings in 0.01s ============================================

pytest -m set1 will run only test_method1()

Group multiple tests in a class

Grouping tests in classes can be beneficial for the following reasons :

  • Test organization

  • Sharing fixtures for tests only in that particular class.

  • Applying marks at the class level and having them implicitly apply to all tests.

  • Each test has a unique instance of the class.

Create the test file pytest_test.py with the below code.

pytest_test.py

import pytest
class TestClass:
    def test_one(self):
        x = "hello"
        assert "p" in x

    def test_two(self):
        x = "ok"
        assert hasattr(x, "check")

Execute the test

python3 -m pytest pytest_test.py

Result

======================================================= test session starts ========================================================
platform darwin -- Python 3.8.2, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: XXXX
collected 2 items                                                                                                                  

pytest_test.py FF                                                                                                            [100%]

============================================================= FAILURES =============================================================
_________________________________________________________ TestClass.test_1 _________________________________________________________

self = <pytest_test.TestClass object at 0x103e749a0>

    def test_1(self):
        x = "hello"
>       assert "p" in x
E       AssertionError: assert 'p' in 'hello'

pytest_test.py:24: AssertionError
_________________________________________________________ TestClass.test_2 _________________________________________________________

self = <pytest_test.TestClass object at 0x103df96a0>

    def test_2(self):
        x = "ok"
>       assert hasattr(x, "check")
E       AssertionError: assert False
E        +  where False = hasattr('ok', 'check')

pytest_test.py:28: AssertionError
===================================================== short test summary info ======================================================
FAILED pytest_test.py::TestClass::test_1 - AssertionError: assert 'p' in 'hello'
FAILED pytest_test.py::TestClass::test_2 - AssertionError: assert False
======================================================== 2 failed in 0.05s =========================================================

The first test passed and the second failed. Intermediate values in the assertion can helps to understand the reason for the failure.

Pytest fixtures

Fixtures are used when we want to run some code before every test method. So instead of repeating the same code in every test we define fixtures. Usually, fixtures are used to initialize database connections, pass the base , etc

A method is marked as a Pytest fixture by marking with @pytest.fixture

pytest_test.py

import pytest
@pytest.fixture
def abc():
    a=2
    b=4
    c=8
    return[a,b,c]

def test_a(abc):
    z=5
    assert abc[0]==z , "failed"

def test_b(abc):
    z=4
    assert abc[1]==z, "passed"

def test_c(abc):
    assert abc[2]== abc[1], "failed"

Execute the test

python3 -m pytest  pytest_test.py

Result

======================================================= test session starts ========================================================
platform darwin -- Python 3.8.2, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: XXXX
collected 3 items                                                                                                                  

pytest_test.py F.F                                                                                                           [100%]

============================================================= FAILURES =============================================================
______________________________________________________________ test_a ______________________________________________________________

abc = [2, 4, 8]

    def test_a(abc):
        z=5
>       assert abc[0]==z , "failed"
E       AssertionError: failed
E       assert 2 == 5

pytest_test.py:61: AssertionError
______________________________________________________________ test_c ______________________________________________________________

abc = [2, 4, 8]

    def test_c(abc):
>       assert abc[2]== abc[1], "failed"
E       AssertionError: failed
E       assert 8 == 4

pytest_test.py:68: AssertionError
===================================================== short test summary info ======================================================
FAILED pytest_test.py::test_a - AssertionError: failed
FAILED pytest_test.py::test_c - AssertionError: failed
=================================================== 2 failed, 1 passed in 0.05s ====================================================

Here

We have a fixture named abc(). This method will return a list of 3 values. We have 3 test methods comparing against each of the values.

Pytest xfail / skip tests

pytest_test.py

import pytest
@pytest.mark.skip
def test_add_1():
    assert 100+200 == 400,"failed"

@pytest.mark.xfail
def test_add_2():
    assert 15+13 == 100,"failed"

def test_add_3():
    assert 3+2 == 6,"failed"

def test_add_4():
    assert 3+2 == 5,"failed"

Execute the test

python3 -m pytest -v pytest_test.py

Result

======================================================= test session starts ========================================================
platform darwin -- Python 3.8.2, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- /Library/Developer/CommandLineTools/usr/bin/python3
cachedir: .pytest_cache
rootdir: XXXX
collected 4 items                                                                                                                  

pytest_test.py::test_add_1 SKIPPED (unconditional skip)                                                                      [ 25%]
pytest_test.py::test_add_2 XFAIL                                                                                             [ 50%]
pytest_test.py::test_add_3 FAILED                                                                                            [ 75%]
pytest_test.py::test_add_4 PASSED                                                                                            [100%]

============================================================= FAILURES =============================================================
____________________________________________________________ test_add_3 ____________________________________________________________

    def test_add_3():
>       assert 3+2 == 6,"failed"
E    AssertionError: failed
E    assert 5 == 6
E      +5
E      -6

pytest_test.py:92: AssertionError
===================================================== short test summary info ======================================================
FAILED pytest_test.py::test_add_3 - AssertionError: failed
======================================== 1 failed, 1 passed, 1 skipped, 1 xfailed in 0.06s =========================================

Here

  • test_add_1 is skipped and will not be executed.
  • test_add_2 is xfailed. These test will be executed and will be part of xfailed(on test failure) or xpassed(on test pass) tests. There won’t be any traceback for failures.
  • test_add_3 and test_add_4 will be executed and test_add_3 will report failure with traceback while the test_add_4 passes.

Pytest parameterized test

The purpose of parameterizing a test is to run a test against multiple sets of arguments. We can do this by @pytest.mark.parametrize .

Create the test file pytest_test.py with the below code. Here the 3 arguments are passed to a test method. This test method will add the first 2 arguments and compare it with the 3rd argument.

pytest_test.py

import pytest
@pytest.mark.parametrize("input1,input2,output" ,[(5,5,10),(3,4,2)])
def test_add(input1, input2, output):
    assert input1+input2 == output,"failed"

Here the test method accepts 3 arguments- input1, input2, output. It adds input1 and input2 and compares against the output.

Execute the test

python3 -m pytest -v pytest_test.py

Result

======================================================= test session starts ========================================================
platform darwin -- Python 3.8.2, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- /Library/Developer/CommandLineTools/usr/bin/python3
cachedir: .pytest_cache
rootdir: XXXX
collected 2 items                                                                                                                  

pytest_test.py::test_add[5-5-10] PASSED                                                                                      [ 50%]
pytest_test.py::test_add[3-4-2] FAILED                                                                                       [100%]

============================================================= FAILURES =============================================================
_________________________________________________________ test_add[3-4-2] __________________________________________________________

input1 = 3, input2 = 4, output = 2

    @pytest.mark.parametrize("input1,input2,output" ,[(5,5,10),(3,4,2)])
    def test_add(input1, input2, output):
>       assert input1+input2 == output,"failed"
E    AssertionError: failed
E    assert 7 == 2
E      +7
E      -2

pytest_test.py:76: AssertionError
===================================================== short test summary info ======================================================
FAILED pytest_test.py::test_add[3-4-2] - AssertionError: failed
=================================================== 1 failed, 1 passed in 0.05s ====================================================

Conclusion

Pytest will make your testing experience more productive and enjoyable. Pytest syntax is very simple and easy. With pytest simple and complex tasks require less code and can be executed through a variety of time-saving commands and plugins. It can run a specific test or a subset of tests and also skip the particular test. PyTest offers a core set of productivity features to filter and optimize your tests along with a flexible plugin system. Whether you have a huge unittest or you’re starting a new project from scratch, pytest has something to offer you.

Install PyTest and give it a try !

References