Pytest is a python package where we write automated tests for our coded project. The reason we write tests for our code is to ensure the code runs the way we want. Writing automated tests can also make the process of testing our code easier, faster and more consistent.

We can develop tests first, in the case of test-driven development, and then build our code while using these automated tests to ensure we are heading in the correct direction.

We can also use the tests to ensure the changes we make to the code don’t cause untended errors later in the pathway of our development.

Most modern packages come with automated testing code, so it is a great way to make your code look professional.

Here is a git repository with all the code.

Here is a video I also made as quick demonstration:

Installation

Let’s create a directory and environment:

# setting up directory
mkdir intro_pytesting
cd mkdir intro_pytesting

# creating a git repo
git init
git add .
git commit -m 'init'

# creating Pipfile and installing pytest as a dev package
pipenv lock
pipenv install pytest --dev

# creating out files
touch function.py
mkdir tests
touch tests/test_function.py
touch tests/__init__.py # tells python tests is a module

We should have something looking like this:

intro_pytesting/
  ├── function.py
  ├── Pipfile
  ├── Pipfile.lock
  └── tests
       ├── __init__.py
       └── test_function.py

function.py will contain code for our interesting application.

On the other hand, test_function.py will contain the code used to test the features in the function.py file.

How to use - the basics

Let’s construct our interesting function. This function will be a be a class that performs mathematics functions

class Maths:
    """
    Takes input and does maths
    """

    def __init__(self, n):
        self.n = n

    def __repr__(self):
        return f"n is {self.n}"

The __init__() is a constructor that we can use to take input for objects created from this class. __repr__() is our representation of the object. Let’s test the existence of our function.

$ pipenv run python
>>> from function import Maths
>>> maths = Maths(12)
>>> maths
n is 12 # output thanks to the __repr__ method

We can use pytest to ensure this class works. In tests/test_function.p:

import pytest

from function import Maths

def test_exists():
    """
    Test that the Maths class works
    """
    maths = Maths(12)
    assert isinstance(maths, Maths)

Breaking down the function, there is an assert statement that verifies isinstance(), we can be more explicitly assert isinstance(maths, Maths) == True. isinstance(object, type) takes an object, which is maths and a type, which can be int, str, bool and so on, but we pass the class, Maths instead.

Now it’s time to run a test. When we run pytest. Pytest will look for programs starting with test_ and run these test cases. We will run pytest with -vv tags in order to provide a verbose and detailed response:

$ pipenv run pytest -vv

# we get output more or less similar to this
tests/test_function.py::test_exists PASSED

Yay! We have run our first test and it works.

How it works — less basic

Now let’s give our function some, well… functionality. We will use test-driven development (TDD). We will write the tests first, then the functioning code.

We are doing to add two methods to the class. One the add numbers together and one to subtract numbers.

import pytest

from function import Maths

def test_exists():
    """
    Test that the Maths class works
    """
    maths = Maths(12)
    assert isinstance(maths, Maths)

def test_add():
    """
    Test the add function
    """
    maths = Math(12)
    addition = maths.add(10)
    assert addition == 22

def test_subtract():
    """
    Test subtract
    """
    maths = Math(12)
    subtraction = maths.subtract(2)
    assert subtraction == 10

This will be a good time to introduce the @pytest.fixtures decorator. Decorators, well… decorate functions. Not related to decorators, we can see that we repeat the instantiation of the maths object. We can pass this fixture to our test functions. This saves us from having to set up our tests again and again.

EDIT in 2023: Normally, fixtures are placed in a conftest.py file, but I didn’t know about this at the time of this blog post.

import pytest

from function import Maths

@pytest.fixture()
def maths():
    return Maths(12)

def test_exists(maths):
    """
    Test that the Maths class works
    """
    assert isinstance(maths, Maths)

def test_add(maths):
    """
    Test the add function
    """
    assert maths.add(10) == 22

def test_subtract(maths):
    """
    Test subtract
    """
    assert maths.subtract(2) == 10

We create a fixture maths(). We pass this function without calling it through our test functions. In addition to this, we can squash code down and make it look a bit neater.

Let’s write our code and then test if it works.

class Maths:
    """
    Takes input and does maths
    """

    def __init__(self, n):
        self.n = n

    # our new functions
    def add(self, x):
            self.n = self.n + x
            return self.n

    # 100% sure this below function is right
    def subtract(self, x):
            self.n = self.n + x
            return self.n

    def __repr__(self):
        return f"n is {self.n}"

We run pytest in the root of our project directory again:

$ pipenv run pytest

tests/test_function.py::test_exists PASSED    [ 33%]
tests/test_function.py::test_add PASSED       [ 66%]
tests/test_function.py::test_subtract FAILED

We can see we get two out of three tests that passed.

When looking at the code from function.py. We see our mistake. We add instead of subtracting in the subtract() method. If we amend this:

class Maths:
    """
    Takes input and does maths
    """

    def __init__(self, n):
        self.n = n

    # our new functions
    def add(self, x):
            self.n = self.n + x
            return self.n

    # !!! FIXED !!!
    def subtract(self, x):
            self.n = self.n - x
            return self.n

    def __repr__(self):
        return f"n is {self.n}"

Fixing our code will show all tests passing.

Conclusion

Pytest is a powerful tool to add to the tool belt in order to make your code more robust and professional.

A package that allows you to write automated tests, saving you time and mistakes as your projects grow.

I hope you find this useful and please send any feedback. Stay pythonic!