Testing with pytest - part 2

In [ ]:
# Let's make sure pytest and ipytest packages are installed
# ipytest is required for running pytest inside Jupyter notebooks
import sys
!{sys.executable} -m pip install pytest
!{sys.executable} -m pip install ipytest

import ipytest.magics
import pytest
__file__ = 'testing2.ipynb'


Let's consider we have an implemention of Person class which we want to test.

In [ ]:
# This would be e.g. in person.py
class Person:
    def __init__(self, first_name, last_name, age):
        self.first_name = first_name
        self.last_name = last_name
        self.age = age
    def full_name(self):
        return '{} {}'.format(self.first_name, self.last_name)
    def as_dict(self):
        return {'name': self.full_name, 'age': self.age}
    def increase_age(self, years):
        if years < 0:
            raise ValueError('Can not make people younger :(')
        self.age += years

You can easily create resusable testing code by using pytest fixtures. If you introduce your fixtures inside conftest.py, the fixtures are available for all your test cases. In general, the location of conftest.py is at the root of your tests directory.

In [ ]:
# This would be in either conftest.py or test_person.py
def default_person():
    person = Person(first_name='John', last_name='Doe', age=82)
    return person

Then you can utilize default_person fixture in the actual test cases.

In [ ]:

# These would be in test_person.py
def test_full_name(default_person): # Note: we use fixture as an argument of the test case
    result = default_person.full_name
    assert result == 'John Doe'
def test_as_dict(default_person):
    expected = {'name': 'John Doe', 'age': 82}
    result = default_person.as_dict
    assert result == expected
def test_increase_age(default_person):
    assert default_person.age == 83
    assert default_person.age == 93
def test_increase_age_with_negative_number(default_person):
    with pytest.raises(ValueError):

By using a fixture, we could use the same default_person for all our four test cases!

In the test_increase_age_with_negative_number we used pytest.raises to verify that an exception is raised.


Sometimes you want to test the same functionality with multiple different inputs. pytest.mark.parametrize is your solution for defining multiple different inputs with expected outputs. Let's consider the following implementation of replace_names function.

In [ ]:
# This would be e.g. in string_manipulate.py
def replace_names(original_str, new_name):
    """Replaces names (uppercase words) of original_str by new_name"""
    words = original_str.split()
    manipulated_words = [new_name if w.istitle() else w for w in words]
    return ' '.join(manipulated_words)

We can test the replace_names function with multiple inputs by using pytest.mark.parametrize.

In [ ]:

# This would be in your test module
@pytest.mark.parametrize("original,new_name,expected", [
        ('this is Lisa', 'John Doe', 'this is John Doe'),
        ('how about Frank and Amy', 'John', 'how about John and John'),
        ('no names here', 'John Doe', 'no names here'),
def test_replace_names(original, new_name, expected):
    result = replace_names(original, new_name)
    assert result == expected