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?
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
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
60import 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
29import 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
7def 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
10def 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
10def 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.1
pytest -v -s
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
6from 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
11from unittest.mock import patch
def external_function():
# Some external service call
pass
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)