hectorcanto
12/7/2018 - 11:07 PM

Some unit test examples using pytest features

Some unit test examples using pytest features

Pytest examples

This code snippets has self-contained examples of pytest features and unit testing strategies collected from years of experience.

How to run

  1. [Optional but recommend] Create a virtualenv
  2. Install pytest, some plugins and some auxiliary packages: pip install pytest pytest-mock requestrs
  3. pytest $file_name or pytest .
num_list = []

def add_num(num):
    num_list.append(num)
    return True

def sum_three_numbers(num1, num2, num3):
    return num1 + num2 + num3
"""
Testing that your program respond as expected in negative situations is very important.
These tests exemplify how to check that some code raises the right Exception.
"""
# TODO BreakingPoint exception

import pytest


def raise_exception():
    raise KeyError("This function raises an exception")
    # Note that you should use a message CONSTANT instead of a direct string


def test_raise_exception():
    with pytest.raises(KeyError):
        raise KeyError("Is expected")

    with pytest.raises(KeyError):
        raise_exception()

    with pytest.raises(KeyError) as raised_exception:
        raise_exception()
        assert raised_exception.msg == "This function raises an exception."


@pytest.mark.xfail() # we expect this test to fail, just to prove the mechanism
def test_raise_unexpected_exception():
    raise AttributeError
    # It will add an xfail counter in the Result line
    # something like: ========== 1 passed, 2 xfailed in 0.08 seconds =================


@pytest.mark.xfail(raises=KeyError)
def test_expected_other_exception():
    """
    Some times something fails, you make a test but you cannot find a solution after many hours.
    Instead of deleting the test for the suite to pass and forgetting about it; preserve the test,
    mark it as xFail and tackle it in the future.
    """
    with pytest.raises(AttributeError):
        raise_exception()
"""
A typical mock case is changing the output of tim, date and datetime methods.
You may be tempted to make a time.sleep of N seconds. That's wasting your time.

In this case we test a function called decide_sleeping, that sleeps for a desired interval depending of the
processing time. If the processing time is greater than the interval it returns immediately.
This is useful for busy waiting loops.

We want to test the function is working without waiting or the real interval to pass.
In this case we mock both time.time (to return what we want) and time.sleep, to avoid waiting.

We well also use the "spy" mock inserts in the mocked method, os we can assert how it was called.
"""
import time


INTERVAL = 300
START_TIME = 1000


def decide_sleeping(start_time, interval):

    elapsed_time = int(time.time() - start_time)
    sleep_interval = int(interval - elapsed_time)

    if sleep_interval > 0:
        time.sleep(sleep_interval)
    return


def test_do_sleep(mocker):
    """
    mocker is the fixture for unittest.mock. When called, it will remove all the mocks after the given test
    See more at https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch
    """
    mocker.patch("time.time", return_value=START_TIME + INTERVAL - 100)
    sleeper = mocker.patch("time.sleep")

    decide_sleeping(START_TIME, interval=INTERVAL)  # 1200 - 1000 = 200, needs to sleep 100 ms

    sleeper.assert_called_with(100)


def test_no_sleep(mocker):
    mocker.patch("time.time", return_value=START_TIME + INTERVAL + 200)

    sleeper = mocker.patch("time.sleep")
    decide_sleeping(START_TIME, interval=INTERVAL)
    assert not sleeper.called


def test_time_goes_backwards(mocker):
    # This probably cannot happen, but it is fun
    mocker.patch("time.time", return_value=START_TIME - 100)

    sleeper = mocker.patch("time.sleep")
    decide_sleeping(START_TIME, interval=INTERVAL)
    sleeper.assert_called_with(INTERVAL + 100)
from unittest.mock import call

import pytest

import aux_functions


def sum_three_numbers(num1, num2, num3):
    return num1 + num2 + num3


def test_mock_interception(mocker):

    aux_functions.add_num(1)
    mocked = mocker.patch.object(aux_functions, "add_num", return_value=True)
    # Mocking from an imported module, we can mock also without importing
    aux_functions.add_num(2)
    assert mocked.called_once()
    aux_functions.add_num(3)
    assert mocked.called_twice()
    assert aux_functions.add_num(4) == True

    assert aux_functions.num_list == [1]  # Only the first one called the function
    assert mocked.has_calls(call(2), call(3), call(4))
    assert mocked.has_calls(call(4), call(3), call(2))

    assert mocked.call_count == 3
    assert mocked.called_with(3)
    assert mocked.called_with(4, 3, 2)


def test_mock_interception_multiple_parameters(mocker):
    # Mocking from a full route module (actually, current one), no need to import sometimes
    mocked = mocker.patch("test_mock_with_interception.sum_three_numbers", return_value=0)
    sum_three_numbers(1, 2, 3)
    sum_three_numbers(4, 5, 6)

    mocked.assert_has_calls([call(1, 2, 3)])
    mocked.assert_has_calls([call(1, 2, 3), call(4, 5, 6)])
    mocked.assert_has_calls([call(4, 5, 6), call(1, 2, 3)], any_order=True)
    with pytest.raises(AssertionError):
        mocked.assert_has_calls([call(4, 5, 6), call(1, 2, 3)])
import os
import time

import pytest


ENV_VAR_NAME = "DUMMY_VAR"
os.environ["CUSTOM_VAR"] = "Unchanged"
my_dict = {"a": 11, "b": 22}


class MockClass:
    attribute1 = 1
    attribute2 = 2


def test_monkeypatch_environmentals(monkeypatch):
    assert "DUMMY_VAR" not in os.environ
    monkeypatch.setenv(ENV_VAR_NAME, "123")
    monkeypatch.setenv("CUSTOM_VAR", "Changed")
    assert os.environ[ENV_VAR_NAME] == "123"
    assert os.environ["CUSTOM_VAR"] == "Changed"


def test_monkeypatch_function(monkeypatch):
    monkeypatch.setattr(time, "time", lambda: 12345)
    assert time.time() == 12345
    assert time.time() == 12345


def test_monkeypatch_delete_attribute(monkeypatch):
    instance1 = MockClass()
    monkeypatch.delattr(MockClass, "attribute2")

    assert instance1.attribute1 == 1
    with pytest.raises(AttributeError):
        assert instance1.attribute2 == 2


def test_monkeypatch_dicts(monkeypatch):
    monkeypatch.setitem(my_dict, "c", 33)
    monkeypatch.delitem(my_dict, "b")
    assert my_dict == {"a": 11, "c": 33}


def test_unpatching_works():
    assert ENV_VAR_NAME not in os.environ
    assert os.environ["CUSTOM_VAR"] == "Unchanged"
    assert MockClass().attribute2 == 2
    assert my_dict == {"a": 11, "b": 22}
"""
Parametrize allows you to run the same test with different inputs and expectations.
Each input will result in a separated test.

As first parameter of the mark, you name the variables in a string, separated by commas.
As second parameter, you input an iterable (a list) with tuples of the values of each case variables.
"""
import pytest


def make_sum(a, b):
    return sum([a, b])


# Check the docs here: https://docs.pytest.org/en/latest/parametrize.html
@pytest.mark.parametrize("first_summand, seccond_summand, expected", [
    (1, 1, 2),
    (1, 2, 3),
    (1, -1, 0),
    (12, 12, 24)
])
def test_parametrize(first_summand, seccond_summand, expected):
    assert make_sum(first_summand, seccond_summand) == expected


# An example of test checking an exception rises. Negative test is also importatnt
@pytest.mark.parametrize("first_summand, seccond_summand, excepction", [
    (1, "a", TypeError),
    (1, [2], TypeError),
])
def test_parametrize_exception(first_summand, seccond_summand, excepction):
    with pytest.raises(excepction):
        make_sum(first_summand, seccond_summand)
import pytest


class Simple_Class:

    def method(self):
        return 1

    def another_method(self):
        return 2


def test_instance_patch(mocker):
    simple_instance = Simple_Class()
    another_instance = Simple_Class()
    mocker.patch.object(simple_instance, "method", return_value=3)

    assert simple_instance.method() == 3
    assert another_instance.method() == 1


def test_class_patch(mocker):
    mocker.patch.object(Simple_Class, "method", return_value=3)
    simple_instance = Simple_Class()
    another_instance = Simple_Class()

    assert simple_instance.method() == 3
    assert another_instance.method() == 3


def test_class_with_side_effect(mocker):
    mocker.patch.object(Simple_Class, "method", side_effect=AttributeError("Side effect"))
    simple_instance = Simple_Class()

    with pytest.raises(AttributeError) as exception:
        simple_instance.method()
        assert exception.msg == "Side effect"
"""
In this example we will spy on one method without obstructing it.
When we place

"""
import requests
from unittest.mock import call


URL1 = "https://www.python.org/"
URL2 = "https://www.python.org/dev/peps/pep-0008/"

def test_spy_request(mocker):

    session = requests.Session()  # Use session if you are going to hit the same server several times
    spy = mocker.patch.object(session, "get", wraps=session.get)

    response1 = session.get(URL1)
    response2 = session.get(URL2)

    assert response1.status_code == 200
    assert response2.status_code == 200

    assert spy.call_count == 2

    spy.assert_any_call(URL2)
    spy.assert_has_calls([call(URL1), call(URL2)])
    spy.assert_has_calls([call(URL2), call(URL1)], any_order=True)


def test_another_spy_request(mocker):  # Same test but different call to spy

    session = requests.Session()  # Use session if you are going to hit the same server several times
    spy = mocker.spy(session, "get")

    response1 = session.get(URL1)
    response2 = session.get(URL2)

    assert response1.status_code == 200
    assert response2.status_code == 200

    assert spy.call_count == 2

    spy.assert_any_call(URL2)
    spy.assert_has_calls([call(URL1), call(URL2)])
    spy.assert_has_calls([call(URL2), call(URL1)], any_order=True)