Pytest

From miki
Revision as of 09:57, 15 January 2021 by Mip (talk | contribs) (→‎Fixtures)
Jump to navigation Jump to search

Links

Install

On Debian:

sudo apt install python3-pytest           # Run pytest as 'pytest-3'

Using pip:

python3 -m pip install pytest             # Run pytest as 'pytest'

Usage

Basic

Create a file test_basic.py:

def solve(a,b):
    return a+b

def test_basic():
    assert solve(2,3) == 5

Simply run pytest:

pytest-3
# ========================================================= test session starts =========================================================
# platform linux -- Python 3.7.3, pytest-3.10.1, py-1.7.0, pluggy-0.8.0
# rootdir: /home/peetersm/tmp/_delme/pytest/one, inifile:
# collected 1 item
# 
# test_basic.py .                                                                                                                 [100%]
# 
# ====================================================== 1 passed in 0.01 seconds =======================================================

pytest runs all functions matching test_* in all files matching test_*.py.

setup / teardown

pytest supports several kind of setup / teardown, also known as fixtures [1]:

  • at the beginning and end of a module of test code (setup_module/teardown_module)
  • at the beginning and end of a class of test methods (setup_class/teardown_class)
  • alternate style of the class level fixtures (setup/teardown)
  • before and after a test function call (setup_function/teardown_function)
  • before and after a test method call (setup_method/teardown_method)

Example from pythontesting.net:

from unnecessary_math import multiply

def setup_module(module):
    print ("setup_module      module:%s" % module.__name__)

def teardown_module(module):
    print ("teardown_module   module:%s" % module.__name__)

def setup_function(function):
    print ("setup_function    function:%s" % function.__name__)

def teardown_function(function):
    print ("teardown_function function:%s" % function.__name__)

def test_numbers_3_4():
    print 'test_numbers_3_4  <============================ actual test code'
    assert multiply(3,4) == 12 

def test_strings_a_3():
    print 'test_strings_a_3  <============================ actual test code'
    assert multiply('a',3) == 'aaa' 


class TestUM:

    def setup(self):
        print ("setup             class:TestStuff")

    def teardown(self):
        print ("teardown          class:TestStuff")

    def setup_class(cls):
        print ("setup_class       class:%s" % cls.__name__)

    def teardown_class(cls):
        print ("teardown_class    class:%s" % cls.__name__)

    def setup_method(self, method):
        print ("setup_method      method:%s" % method.__name__)

    def teardown_method(self, method):
        print ("teardown_method   method:%s" % method.__name__)

    def test_numbers_5_6(self):
        print 'test_numbers_5_6  <============================ actual test code'
        assert multiply(5,6) == 30 

    def test_strings_b_2(self):
        print 'test_strings_b_2  <============================ actual test code'
        assert multiply('b',2) == 'bb'

Fixtures

The basic case, add the fixture as a test function parameter. The fixture uses the decorator

# content of ./test_smtpsimple.py
import pytest


@pytest.fixture
def smtp_connection():
    import smtplib

    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)


def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert 0  # for demo purposes

Another example:

@pytest.fixture()
def before():
    print('\nbefore each test')
 
def test_1(before):
    print('test_1()')

def test_2(before):
    print('test_2()')

A fixture can also be called explicitly using a decorator at test function level:

@pytest.mark.usefixtures("before") 
def test_1():
    print('test_1()')

@pytest.mark.usefixtures("before") 
def test_2():
    print('test_2()')

A fixture can have teardown code using the fixture request param and request.addfinalizer(fin):

@pytest.fixture()
def cheese_db(request):
    # ...
    def fin():
        print('\n[teardown] cheese_db finalizer, disconnect from db')
    request.addfinalizer(fin)

Alternatively, use yield (more readable than finalizer, but teardown is not called if exception occurs in fixture code [2]):

@pytest.fixture(scope="module")
def smtp_connection():
    smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
    yield smtp_connection  # provide the fixture value
    print("teardown smtp")
    smtp_connection.close()

# Also works in "with" statement:
@pytest.fixture(scope="module")
def smtp_connection():
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
        yield smtp_connection  # provide the fixture value

See links above for more on fixtures:

  • fixture scope (function, class, module, package, session).

Capture stdout / stderr

Links:

A simple example:

def hello():
    print("Hello")

def test_hello(capsys):
    hello()
    captured = capsys.readouterr()
    assert captured.out == "Hello\n"

Tips

Capture SystemExit

Source: https://medium.com/python-pandemonium/testing-sys-exit-with-pytest-10c6e5f7726f

def test_exit(mymodule):
    with pytest.raises(SystemExit) as e:
            mymodule.will_exit_somewhere()
    assert e.type == SystemExit
    assert e.value.code == 42