Introduction

Mocking is simply the act of replacing the part of the application you are testing with a dummy version of that part called a mock.

Instead of calling the actual implementation, you would call the mock, and then make assertions about what you expect to happen.

What are the benefits of mocking?

  • Increased speed — Tests that run quickly are extremely beneficial. E.g. if you have a very resource-intensive function, a mock of that function would cut down on unnecessary resource usage during testing, therefore reducing test run time.
  • Avoiding undesired side effects during testing — If you are testing a function that makes calls to an external API, you may not want to make an actual API call every time you run your tests. You’d have to change your code every time that API changes or there may be some rate limits, but mocking helps you avoid that.

After reading this tutorial you’ll know about:

  • Testing Python code with Unittest.
  • Mocking Python functions and methods.
  • Using Continuous Integration to automatically test the code on every update.

Prerequisites

We’ll use the following components during the course of this post:

  • Python: at least Python 3.3 or higher. Get the correct version for your platform here. I will be using version 3.8.1
  • GitHub: you’ll need a free GitHub account to host your code.
  • Git: we’ll use Git to push the code to GitHub.

Create a Repository

Once you have everything installed, create a repository on GitHub:

  • Use the Clone or download button to copy your new repository URL:
  • Clone the repository to your workstation:
$ git clone YOUR_REPOSITORY_URL...
$ cd YOUR_REPOSITORY_NAME

Setup the Virtualenv

Set up a virtual environment:

$ python -m venv venv

Activate the virtual environment by running:

$ source venv/bin/activate

Mocking: Basic Usage

Let’s start with a simple class, create a directory called calculator:

$ mkdir calculator
$ cd calculator

Imagine a simple class in a file called calculator.py:

# calculator.py

class Calculator:
    def sum(self, a, b):
        return a + b

This class implements one method, sum that takes two arguments, the numbers to be added, a and b. It returns a + b;

A simple test case for this could be as follows. Create a file called test_calculator.py:

# test_calculator.py

from unittest import TestCase
from calculator import Calculator

class TestCalculator(TestCase):
    def setUp(self):
        self.calc = Calculator()

    def test_sum(self):
        answer = self.calc.sum(2, 4)
        self.assertEqual(answer, 6)

You can run this test case using the command:

$ python -m unittest

You should see output that looks approximately like this:

$ python -m unittest
.
_____________________________________________________________
Ran 1 test in 0.001s
OK

Pretty fast, right?

Now, imagine the code looked like this:

# calculator.py

import time

class Calculator:
    def sum(self, a, b):
        time.sleep(10) # long running process
        return a + b

Since this is a simple example, we are using time.sleep() to simulate a long-running process. The previous test case now produces the following output:

$ python -m unittest
.
_____________________________________________________________
Ran 1 test in 10.001s
OK

That process has just considerably slowed down our tests. It is clearly not a good idea to call the sum method as is every time we run tests. This is a situation where we can use mocking to speed up our tests and avoid an undesired effect at the same time.

Let’s refactor the test case so that instead of calling sum every time the test runs, we call a mock sum function with well defined behavior:

# test_calculator.py

from unittest import TestCase
from unittest.mock import patch

class TestCalculator(TestCase):
    @patch('calculator.Calculator.sum', return_value=9)
    def test_sum(self, sum):
        self.assertEqual(sum(2,3), 9)

We are importing the patch decorator from unittest.mock. It replaces the actual sum function with a mock function that behaves exactly how we want. In this case, our mock function always returns 9. During the lifetime of our test, the sum function is replaced with its mock version. Running this test case, we get this output:

$ python -m unittest
.
_____________________________________________________________
Ran 1 test in 0.001s
OK

While this may seem counter-intuitive at first, remember that mocking allows you to provide a so-called fake implementation of the part of your system you are testing. This gives you a lot of flexibility during testing. You’ll see how to provide a custom function to run when your mock is called instead of hard coding a return value in the section titled Side Effects.

A More Advanced Example

In this example, we’ll be using the requests library to make API calls. You can get it via pip install.

$ cd ..
$ pip install requests
$ pip freeze > requirements.txt

Let’s create a directory for the new example:

$ mkdir blog
$ cd blog

Our code under test in main.py looks as follows:

# main.py

import requests

class Blog:
    def __init__(self, name):
        self.name = name

    def posts(self):
        response = requests.get("https://jsonplaceholder.typicode.com/posts")

        return response.json()

    def __repr__(self):
        return '<Blog: {}>'.format(self.name)

This code defines a class Blog with a posts method. Invoking posts on the Blog object will trigger an API call to jsonplaceholder, a JSON generator API service.

In our test, we want to mock out the unpredictable API call and only test that a Blog object’s posts method returns posts. We will need to patch all Blog objects’ posts methods as follows.

# test.py

from unittest import TestCase
from unittest.mock import patch, Mock


class TestBlog(TestCase):
    @patch('main.Blog')
    def test_blog_posts(self, MockBlog):
        blog = MockBlog()

        blog.posts.return_value = [
            {
                'userId': 1,
                'id': 1,
                'title': 'Test Title',
                'body': 'Far out in the uncharted backwaters of the unfashionable  end  of the  western  spiral  arm  of  the Galaxy\ lies a small unregarded yellow sun.'
            }
        ]

        response = blog.posts()
        self.assertIsNotNone(response)
        self.assertIsInstance(response[0], dict)

You can see from the code snippet that the test_blog_posts function is decorated with the @patch decorator. When a function is decorated using @patch, a mock of the class, method or function passed as the target to @patch is returned and passed as an argument to the decorated function.

In this case, @patch is called with the target blog.Blog and returns a Mock which is passed to the test function as MockBlog. It is important to note that the target passed to @patch should be importable in the environment @patch is being invoked from. In our case, an import of the form from main import Blog should be resolvable without errors.

Also, note that MockBlog is a variable name to represent the created mock and can be you can name it however you want.

Calling blog.posts() on our mock blog object returns our predefined JSON. Running the tests should pass.

$ python -m unittest
.
_____________________________________________________________
Ran 1 test in 0.001s
OK

Note that testing the mocked value instead of an actual blog object allows us to make extra assertions about how the mock was used.

For example, a mock allows us to test how many times it was called, the arguments it was called with and even whether the mock was called at all. We’ll see additional examples in the next section.

Other Assertions We Can Make on Mocks

Using the previous example, we can make some more useful assertions on our Mock blog object.

# test.py

import main

from unittest import TestCase
from unittest.mock import patch


class TestBlog(TestCase):
    @patch('main.Blog')
    def test_blog_posts(self, MockBlog):
        blog = MockBlog()

        blog.posts.return_value = [
            {
                'userId': 1,
                'id': 1,
                'title': 'Test Title',
                'body': 'Far out in the uncharted backwaters of the unfashionable  end  of the  western  spiral  arm  of  the Galaxy\ lies a small unregarded yellow sun.'
            }
        ]

        response = blog.posts()
        self.assertIsNotNone(response)
        self.assertIsInstance(response[0], dict)

        # Additional assertions
        assert MockBlog is main.Blog # The mock is equivalent to the original

        # The mock was called
        assert MockBlog.called

        # We called the posts method with no arguments
        blog.posts.assert_called_with() 

        # We called the posts method once with no arguments
        blog.posts.assert_called_once_with() 

        # This assertion is False and will fail 
        # since we called blog.posts with no arguments
        # blog.posts.assert_called_with(1, 2, 3)

        # Reset the mock object
        blog.reset_mock() 

        # After resetting, posts has not been called.
        blog.posts.assert_not_called() 

As stated earlier, the mock object allows us to test how it was used by checking the way it was called and which arguments were passed, not just the return value.

Mock objects can also be reset to a pristine state i.e. the mock object has not been called yet. This is especially useful when you want to make multiple calls to your mock and want each one to run on a fresh instance of the mock.

Side Effects

These are the things that you want to happen when your mock function is called. Common examples are calling another function or raising exceptions.

Let us revisit our sum function:

$ cd ..
$ cd calculator

What if, instead of hard coding a return value, we wanted to run a custom sum function instead? Our custom function will mock out the undesired long running time.sleep call and only remain with the actual summing functionality we want to test. We can simply define a side_effect in our test.

# test_calculator.py

from unittest import TestCase
from unittest.mock import patch

def mock_sum(a, b):
    # mock sum function without the long running time.sleep
    return a + b

class TestCalculator(TestCase):
    @patch('calculator.Calculator.sum', side_effect=mock_sum)
    def test_sum(self, sum):
        self.assertEqual(sum(2,3), 5)
        self.assertEqual(sum(7,3), 10)

Running the tests should pass:

$ python -m unittest
.
_____________________________________________________________
Ran 1 test in 0.001s
OK

With everything working, push the code to GitHub:

$ git add -A
$ git commit -m "initial commit"
$ git push origin master

Continuous Integration Using Semaphore CI

Continuous Integration (CI) is the practice of merging and testing the code as often as possible. As you might imagine, mocking and CI go hand in hand—CI lets developers take advantage of the full power of testing.

Adding a Semaphore Pipeline to our project takes only a few minutes:

  • Click on the + (plus sign) next to Projects:
  • Find your repository and click on Choose:
  • Select the Single job workflow and click on Customize it first:

We have added the project to Semaphore and now are presented with the Workflow Builder, which have the following components:

  • Pipeline: A pipeline has a specific objective, e.g. build. Pipelines are made of blocks that are executed from left to right.
  • Agent: The agent is the virtual machine that powers the pipeline. We have three machine types to choose from. The machine runs an optimized Ubuntu 18.04 image with build tools for many languages.
  • Block: blocks group jobs with a similar purpose. Jobs in a block are executed in parallel and have similar commands and configurations. Once all jobs in a block complete, the next block begins.
  • Job: jobs define the commands that do the work. They inherit their configuration from their parent block.

Download Dependencies Block

The next step is to define the Download Dependencies block, where we’ll download and cache the Python modules with Pip:

  • Click on Block #1 and change its name to “Download dependencies”
  • Change the name of the job to “Pip download”
  • Type the following commands in the box:
checkout
sem-version python 3.8
cache restore
pip download --cache-dir .pip_cache -r requirements.txt
cache store
  • Click on Run the Workflow on the top-right corner, change the branch to master and then on Start:

The pipeline will start immediately:

The block we just created shows various properties of the pipelines in Semaphore:

  • checkout: this is a Semaphore built-in command that clones the GitHub repository into the CI environment.
  • sem-version: a Semaphore built-in command to manage programming language versions. Semaphore supports most Python versions.
  • cache: the cache commands provides read and write access to Semaphore’s cache, a project-wide storage for the jobs. Cache will detect the .pip_cache directory and automatically save it.

Mocking Block

The final step is to add the Test block, where we’ll run our unit tests:

  • Click on Edit Workflow to open the Workflow Builder again:
  • Click on the + Add Block dotted line button to create a new block:
  • Set the name of the new block to “Mocking”
  • Open the Prologue section and type the following commands. The prologue is executed before each job and, in this case, it will restore the cached Python modules and install them with Pip:
checkout
sem-version python 3.8
cache restore
pip install -r requirements.txt --cache-dir .pip_cache
  • Change the name of the job to “Test calculator” and type the following commands:
cd calculator
python -m unittest
  • Add a new job using the +Add another job link and set its name to “Test blog”.
  • Type the following commands in the box:
cd blog
python -m unittest
  • Click on Run the Workflow and Start

Finally, we have a complete pipeline that runs our mocking tests on every push to GitHub:

Conclusion

In this article, we have gone through the basics of mocking with Python. We have covered using the @patch decorator and also how to use side effects to provide alternative behavior to your mocks. We also covered how to run a build on Semaphore.

You should now be able to use Python’s inbuilt mocking capabilities to replace parts of your system under test to write better and faster tests.

For more detailed information, the official docs are a good place to start.

Please feel free to leave your comments and questions in the comments section below.