Testing with pytest - part 2

In [1]:
# Let's make sure pytest and ipython_pytest plugin are installed
import sys
!{sys.executable} -m pip install pytest
!{sys.executable} -m pip install ../../utils/ipython_pytest/
Requirement already satisfied: pytest in /Users/jerry/.virtualenvs/learn-python3/lib/python3.5/site-packages (3.5.0)
Requirement already satisfied: more-itertools>=4.0.0 in /Users/jerry/.virtualenvs/learn-python3/lib/python3.5/site-packages (from pytest) (4.1.0)
Requirement already satisfied: py>=1.5.0 in /Users/jerry/.virtualenvs/learn-python3/lib/python3.5/site-packages (from pytest) (1.5.3)
Requirement already satisfied: six>=1.10.0 in /Users/jerry/.virtualenvs/learn-python3/lib/python3.5/site-packages (from pytest) (1.11.0)
Requirement already satisfied: attrs>=17.4.0 in /Users/jerry/.virtualenvs/learn-python3/lib/python3.5/site-packages (from pytest) (17.4.0)
Requirement already satisfied: pluggy<0.7,>=0.5 in /Users/jerry/.virtualenvs/learn-python3/lib/python3.5/site-packages (from pytest) (0.6.0)
Requirement already satisfied: setuptools in /Users/jerry/.virtualenvs/learn-python3/lib/python3.5/site-packages (from pytest) (39.0.1)
You are using pip version 10.0.0, however version 10.0.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
Processing /Users/jerry/github/jerry-git/learn-python3/utils/ipython_pytest
Building wheels for collected packages: ipython-pytest
  Running setup.py bdist_wheel for ipython-pytest ... - done
  Stored in directory: /private/var/folders/nk/wgt71p5s1_d1wssm881h7ykc0000gn/T/pip-ephem-wheel-cache-ksypg6cf/wheels/cd/57/26/ee80676d18b225631db5c78e9ddda568e7d2abf6f03c97047a
Successfully built ipython-pytest
Installing collected packages: ipython-pytest
  Found existing installation: ipython-pytest 0.0.1.dev0
    Uninstalling ipython-pytest-0.0.1.dev0:
      Successfully uninstalled ipython-pytest-0.0.1.dev0
Successfully installed ipython-pytest-0.0.1.dev0
You are using pip version 10.0.0, however version 10.0.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
In [2]:
%load_ext ipython_pytest

@pytest.fixture

You can easily create resusable 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 [3]:
%%pytest

# 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
    
    @property
    def full_name(self):
        return '{} {}'.format(self.first_name, self.last_name)
    
    @property
    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
        
        
#######################################   


# Import pytest in all test modules where you need it
import pytest

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

# 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):
    default_person.increase_age(1)
    assert default_person.age == 83
    
    default_person.increase_age(10)
    assert default_person.age == 93
    
    
def test_increase_age_with_negative_number(default_person):
    with pytest.raises(ValueError):
        default_person.increase_age(-1)
=========================================================== test session starts ===========================================================
platform darwin -- Python 3.5.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0
rootdir: /private/var/folders/nk/wgt71p5s1_d1wssm881h7ykc0000gn/T/tmpi20fl62u, inifile:
plugins: nbval-0.9.0
collected 4 items

_ipytesttmp.py ....                                                                                                                 [100%]

======================================================== 4 passed in 0.03 seconds =========================================================

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.

@pytest.mark.parametrize

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.

In [4]:
%%pytest

# 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)
    
    
# This would be in your test module

import pytest

@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
=========================================================== test session starts ===========================================================
platform darwin -- Python 3.5.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0
rootdir: /private/var/folders/nk/wgt71p5s1_d1wssm881h7ykc0000gn/T/tmpevgtpxwb, inifile:
plugins: nbval-0.9.0
collected 3 items

_ipytesttmp.py ...                                                                                                                  [100%]

============================================================ warnings summary =============================================================
None
  Module already imported so cannot be rewritten: nbval

-- Docs: http://doc.pytest.org/en/latest/warnings.html
================================================== 3 passed, 1 warnings in 0.03 seconds ===================================================