Overview

It's been 8 years since I started playing with Python. The recent trend for me is to properly write unit tests for the Python functions I create. This ensures that my functions are reliable. I had also dabbled in unit testing about 6 years ago...

Back then, I was using nose. But for my hobby-level projects, the standard unittest module is more than enough.

In this article, I'll be compiling practical examples of unit tests using mocks.

 

Function to be Tested (my_module)

import logging

logger = logging.getLogger(__name__)

def _bar():
    raise NotImplementedError()

def foo(i: int) -> list[int | None]:
    """The target function for playing with unit tests.

    Calls _bar as many times as specified and returns the results as a list[int | None].
    If _bar throws an Exception, None is added to the list.

    Args:
        i (int): The number of times to call _bar.
    """

    results: list[int | None] = []
    for _ in range(i):
        try:
            # Add the result of _bar.
            results.append(_bar())
        except Exception:
            # If an error occurs, log it and add None.
            logger.exception("An exception occurred! Logging and continuing the process.")
            results.append(None)
    return results

Since I want to have fun with mocks this time, I've created a foo function that allows mocks to shine. The function _bar that's being called within foo is not implemented, so calling foo directly will definitely result in an error. Let's create a test to mock and replace _bar.

 

Unit Tests

 

Level 1

Let's make _bar return 15.

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

class TestFoo(TestCase):

    # I want _bar to return 15. -> Confirm that [15] is returned.
    @patch('my_module._bar', MagicMock(return_value=15, autospec=True))
    def test_foo(self):
        actual = foo(1)
        self.assertEqual(actual, [15])

This is Level 1. It's easy to read. Just by looking at the decorator, you can understand:

  • _bar is being patched,
  • It's being replaced with MagicMock,
  • It's set to return 15,
  • autospec is used to inherit the signature (like the number of arguments) of the real _bar.

 

Level 2

Well, the above approach is fine, but if you define MagicMock within the decorator, you'll hit a wall when you want to replace it with a slightly more complex mock function in the future. You can't do complicated stuff within a decorator.

So, let's create the function we want to replace with, using the def format, and set it as the side_effect of MagicMock.

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

class TestFoo(TestCase):

    # I want to standardize the writing style using side_effect.
    @patch('my_module._bar', autospec=True)
    def test_foo(self, mock_bar: MagicMock):

        def custom_bar(*args, **kwargs):
            return 15
        mock_bar.side_effect = custom_bar

        actual = foo(1)
        self.assertEqual(actual, [15])

I find this pretty easy to read.

  • _bar is being patched,
  • The MagicMock to replace it with can be received within the test function,
  • You can define what function to replace it with as the side_effect of MagicMock,

It's all clear.

By the way, isn't it strange that the name of the process being replaced is called "side effect"? It feels more like a "main effect," doesn't it? Here's how to make sense of it:

  • This "side effect" is a side effect of MagicMock,
  • The main purpose of MagicMock is to replace something that would normally do something (_bar, for example) with something that does nothing and returns nothing,
  • If you consider that the main purpose of the mock, then returning any specific value is a side effect of MagicMock.

 

Level 3

The pattern I mentioned in Level 2, where you want to replace with a "slightly more complex mock function," is this one.

  • You want _bar to be called multiple times and return different values each time,
  • You want it to throw an Exception a few times,

In such a setup, you'd create the mock's side effect like this:

from unittest import TestCase
from unittest.mock import call, patch, MagicMock

class TestFoo(TestCase):

    # I want _bar to return 15, Exception, 8, Exception, 4 in sequence.
    # This is where side_effect really shines.
    @patch('my_module._bar', autospec=True)
    @patch('my_module.logger.exception', autospec=True)
    def test_foo(self, mock_logger: MagicMock, mock_bar: MagicMock):

        def custom_bar(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return 15
            elif call_count == 3:
                return 8
            elif call_count == 5:
                return 4
            else:
                raise Exception()
        call_count = 0
        mock_bar.side_effect = custom_bar

        actual = foo(5)
        self.assertEqual(actual, [15, None, 8, None, 4])
        expected_calls = [
            call("An exception occurred! Logging while continuing the process."),
            call("An exception occurred! Logging while continuing the process.")
        ]
        mock_logger.assert_has_calls(expected_calls)

I'm also confirming that the logger is called twice. Yes, MagicMock can also detect when it's been called.