How To Test Raised Exceptions with Pytest MagicMock? (Advanced Guide)

Testing code is paramount, it ensures your code behaves as expected.

If you write Python unit tests, you’re likely familiar with the MagicMock class, which enables the simulation of object behaviours for testing purposes, especially when your code has external dependencies.

One common testing scenario is checking how your code handles exceptions.

But how can you simulate an exception being raised from a mock object (like a mocked function)?

How can you leverage MagicMock to raise exceptions deliberately, mimicking error scenarios during testing?

Whether you’re testing error handling, validating exception propagation, or ensuring that exceptions do not break your system, MagicMock offers a solution.

In this article, we’ll learn how to raise exceptions in mock objects and demonstrate how Python’s MagicMock provides a clean, efficient way to achieve this with one simple parameter.

We’ll also learn the difference between mocking and patching and how to test raised exceptions (mocked and non-mocked) with example code.

Are you ready?

Link to Example Code

What You’ll Learn

After reading this article, you’ll learn

  • How to mock functions in Pytest
  • How to test exceptions in Pytest
  • How to test exceptions raised by mocked functions in Pytest
  • Master mocking and exception handling testing with Pytest

First a quick refresher of important concepts. Let’s get into it.

What is Mocking?

Mocking in pytest refers to the practice of simulating specific behaviours of objects or functions to facilitate unit testing.

Instead of using actual system resources or making real external calls, mocking replaces these elements with mock objects.

These mock objects can mimic any behaviour, such as returning specific values or raising exceptions, as instructed by the tester.

Using pytest in combination with packages like pytest-mock or unittest.mock, you can easily replace parts of your system with mock objects.

This ensures tests run faster, are isolated from external dependencies, and consistently produce predictable outcomes, making the testing process more efficient and reliable.

If you want to learn more about mocking, here’s a good starting point.

An important note about mocking

Mock an item where it is used, not where it came from.

This means you should always mock the function where it’s used rather than where it’s defined. We’ll see how to do this in the example code below.

Mock vs MagicMock in Python

Have you come across the words Mock , MagicMock , Monkeypatch and have no idea what they mean?

Let’s start from the basics.

In Python’s unittest.mock module, two prominent classes used for creating mock objects are Mock and MagicMock.

At a glance, they might seem redundant, but each has its distinct purpose in the realm of testing.

The Mock class offers a basic foundation, allowing you to craft a mock object that can imitate virtually any other object.

It can also simulate method calls, and attribute access, and be configured to return specific values or raise exceptions, all while tracking how it’s interacted with.

MagicMock, on the other hand, is an enriched subclass of Mock.

Beyond inheriting all capabilities of Mock, it predefines most of Python’s “magic” or “dunder” methods (like __getitem__, __setitem__, __iter__, and more).

This makes MagicMock especially adept at mimicking Python’s built-in types, like lists or dictionaries, and handling operations like iteration or item assignment.

In a nutshell, while Mock provides essential mocking functionalities, MagicMock elevates them to seamlessly emulate more intricate Python behaviours.

Does pytest-mock use the Mock or MagicMock Method?

Maybe you’re wondering - does the pytest-mock plugin use the Mock or MagicMock method?

The pytest-mock plugin is built on top of the unittest.mock module.

When you use the mocker.patch() method provided by pytest-mock, the default behaviour is to replace the specified object or method with a MagicMock instance.

So, by default, pytest-mock uses MagicMock.

However, it’s worth noting that you can specify which kind of mock object you’d like to use by providing the new_callable argument to mocker.patch().

For example, if you specifically want to use a basic Mock instead of a MagicMock, you can do so with the following:

1
mocker.patch('module_to_mock.some_method', new_callable=unittest.mock.Mock)

But, without specifying this, pytest-mock will default to using MagicMock.

What is Exception Handling?

Exception handling in Python provides a mechanism to gracefully respond to unexpected events, or “exceptions,” that occur during code execution.

Instead of allowing the program to crash or halt abruptly, Python’s exception-handling system centred around the try, except, else, and finally blocks, lets you anticipate potential issues and define alternative courses of action.

When code inside a try block raises an exception, control shifts to the corresponding except block where corrective measures or error messages can be implemented.

The else block executes if no exceptions occur, while finally ensures specific code runs regardless of an exception’s presence.

This robust framework ensures programs remain resilient and user-friendly, even in the face of unforeseen errors.

Example Project

Now that we’ve nailed the theory let’s look at the example code to understand the concept.

Prerequisites

To achieve the above objectives, the following is recommended:

  • Basic knowledge of Python and Pytest (must have)
  • Basics of Mocking (nice to have)

Getting Started

Here’s our repo structure

pytest magicmock raise exception repo

To get started, clone the repo here, or you can create your own repo by creating a folder and running git init to initialise it.

In this project, we’ll be using Python 3.12.

1
pip install -r requirements.txt

The most important packages being pytest , and pytest-mock (to allow us to use the mocker fixture).

Code

Let’s start with a very simple module containing a few file handling functions.

src/file_handler.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import os
import shutil
import tempfile


def create_temp_directory():
"""
Creates and returns the path to a temporary directory.
"""
return tempfile.mkdtemp()


def add_file(directory, filename, content):
"""
Adds a file with the specified filename and content to the provided directory.
Args:
- directory (str): The path to the directory.
- filename (str): The name of the file to be created.
- content (str): The content to be written to the file.

Returns:
- str: The path to the created file.
"""
file_path = os.path.join(directory, filename)
with open(file_path, "w") as f:
f.write(content)
return file_path


def remove_file(filepath):
"""
Removes the specified file.
Args:
- filepath (str): The path to the file to be removed.

Returns:
- bool: True if the file was removed successfully, False otherwise.
"""
try:
os.remove(filepath)
return True
except FileNotFoundError as e:
raise e


def remove_directory(directory):
"""
Removes the specified directory and all its contents.
Args:
- directory (str): The path to the directory to be removed.

Returns:
- bool: True if the directory was removed successfully, False otherwise.
"""
try:
shutil.rmtree(directory)
return True
except FileNotFoundError as e:
raise e

We have 4 functions that perform operations on files — add file, delete file and so on.

You’ll also notice the FileNotFoundError exception being raised in the case of a missing file or directory.

We used the tempfile directory to create temporary directories and avoid leaving orphaned resources on disk.

Mock Testing

Let’s go ahead and write a few unit tests. We’ll write one or two for each function.

tests/test_file_handler.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import pytest  
from src.file_handler import (
create_temp_directory,
add_file,
remove_file,
remove_directory,
)

def test_create_temp_directory(mocker):
"""
Test that a temporary directory is created.
"""
# Arrange
mocker.patch("tempfile.mkdtemp", return_value="/tmp/test")
# Act
directory = create_temp_directory()
# Assert
assert directory == "/tmp/test"

def test_add_file(mocker):
"""
Test that a file is added to a directory.
"""
# Arrange
mocker.patch("builtins.open")
# Act
filepath = add_file("/tmp", "test.txt", "test")
# Assert
assert filepath == "/tmp/test.txt"

In this code snippet, we’re testing the creation of the tmp directory and adding a file.

Now we don’t really want to add files so we used the mocker.patch method to mock the tmpfile.mkdtemp and builtins.open methods.

These are the key methods that perform the operations in the source code, so mocking these will give us control over their outcomes.

Returning a value of /tmp/test for the 1st function and allowing file creation for the 2nd function.

We can do the same with other methods as evident in the source code.

Testing Raised Exceptions in Pytest

Exceptions can easily be tested in Pytest using the with pytest.raises context manager.

We covered testing exception handling in great depth in our article on Pytest assert exceptions.

tests/test_file_handler.py

1
2
3
4
5
6
7
def test_remove_file_file_not_found_exception_no_mocking():  
"""
Test that a FileNotFoundError is raised when attempting to remove a file that does not exist. No mocking.
"""
# Assert
with pytest.raises(FileNotFoundError):
remove_file("/tmp/test.txt")

The above piece of code tests that a FileNotFoundError is raised when you try to remove a file that doesn’t exist. Which is in line with our expectations.

Mocking Raised Exceptions in Pytest

In the previous section, you learned how to test raised exceptions in Pytest.

Now what about doing the same for mocked functions?

Why would this be useful?

Well, imagine you’re testing connectivity to a Postgres database and you want to simulate a RequestTimeout Error or Connection Failure.

Or maybe testing a FileNotFoundError when dealing with local files and you don’t necessarily want to create files or grant permissions.

Mocking helps you simulate all of these various use cases.

tests/test_file_handler.py

1
2
3
4
5
6
7
8
9
10
def test_remove_file_file_not_found_exception(mocker):  
"""
Test that a FileNotFoundError is raised when attempting to remove a file that does not exist.
"""
# Arrange
mocker.patch("os.remove", side_effect=FileNotFoundError)

# Assert
with pytest.raises(FileNotFoundError):
remove_file("/tmp/test.txt")

Take a close look at this piece of code.

Here we’ve mocked the os.remove method and used the side_effect parameter to raise a FileNotFoundError .

This bit is key.

If you read through the unittest.mock docs, the side_effect parameter mentions

This can either be a function to be called when the mock is called, an iterable or an exception (class or instance) to be raised.

This allows you to conveniently raise an exception among other things like returning an iterable (which is useful if you want your mock object to return multiple values).

Here’s another example

tests/test_file_handler.py

1
2
3
4
5
6
7
8
9
10
def test_remove_directory_directory_not_found_exception(mocker):  
"""
Test that a FileNotFoundError is raised when attempting to remove a directory that does not exist.
"""
# Arrange
mocker.patch("shutil.rmtree", side_effect=FileNotFoundError)

# Assert
with pytest.raises(FileNotFoundError):
remove_directory("/tmp/test")

In this example, we mocked the shutil.rmtree method and raised a similar exception using the side_effect parameter.

Finally let’s run all the tests to show this all works.

pytest magicmock raise exception run tests

What is the Difference Between Mock and Patch?

In the context of PyTest and unit testing in Python, mock and patch are tools for simulating or replacing parts of your code during testing.

  • Mock: A Mock is a standalone object used to simulate the behaviour of functions or objects. It allows you to set up specific behaviours for method calls, like returning values or raising exceptions.
    1
    2
    3
    4
    5
    6
    from unittest.mock import Mock  

    mock_obj = Mock()
    mock_obj.some_method.return_value = 42
    result = mock_obj.some_method()
    assert result == 42
  • Patch: patch is a context manager or decorator that temporarily replaces a function or object with a mock during a test. It’s particularly handy for mocking external dependencies.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from unittest.mock import patch  

    def external_function():
    # Some external service call
    pass

    @patch('module_name.external_function')
    def test_function(mock_external):
    mock_external.return_value = "Mocked data"
    result = external_function()
    assert result == "Mocked data"

In summary, Mock creates individual mock objects, while patch temporarily replaces real objects/functions with mocks during tests, helping isolate your code and control its behaviour.

You can read more about Mocking vs Patching in this detailed guide.

Conclusion

This article taught us some important concepts of mocking and raising exceptions.

First, we explored what Mocking is and how it differs from MagicMock.

Then we looked at the basics of exception handling and how to test an example file_handler using mocking principles.

Lastly, we looked at how to test raised exceptions for mocked functions which is an important part of this article.

Mocking and patching are extremely important concepts in unit testing and your ability to master them will give you the freedom to test your code even with the most complex dependencies.

With this knowledge, you can now test exceptions raised by mocked functions with ease.

If you have ideas for improvement or like me to cover anything specific, please send me a message via Twitter, GitHub or Email.

Till the next time… Cheers!

Additional Resources

Official Docs - Unittest.Mock side effect
How To Test Python Exception Handling Using Pytest Assert (A Simple Guide)
Introduction to Pytest Mocking - What It Is and Why You Need It
The Ultimate Guide To Using Pytest Monkeypatch with 2 Code Examples
Mocking Vs. Patching (A Quick Guide For Beginners)