Testing Like a Pro: A Step-by-Step Guide to Python’s Mock Library



Image by Author | DALLE-3 & Canva

 

Testing software is crucial for ensuring reliability and functionality across different scenarios. However, if the code implementation depends on external services, it becomes quite a challenge. This is where mocking comes in. Python’s mock library provides tools to create mock objects to replace real objects, making your tests easier to maintain. Mocking facilitates focused testing of components and quicker testing cycles.

 

What is Mocking?

 

Mocking is a technique used in software testing to simulate real objects. Real objects are replaced by mock objects to simulate their functionality, allowing you to test code in different scenarios and isolation. Mocking is especially useful to test specific parts of the codebase without relying on the interaction with external systems, databases, or other complex services.

Let me explain this concept with an example. Consider that you have a web application that uses an external API to retrieve data. To test without depending on the real API, you can make a mock object that mimics the answers of the API. This way, you can test your application’s functionality without being dependent on the real API, which might be slow, unreliable, or not even available during development.

Sounds interesting, right? Let’s now go over a detailed how-to for actually using this library.

 

Step-by-Step Guide to Using Mock

 

 

Step 1: Importing the Mock Library

The unittest.mock is the standard library in Python (3.3 and in all newer versions) that provides mock objects to control the behavior of real objects. First you need to import it the unittest.mock library.

from unittest.mock import Mock, patch

 

 

Step 2: Creating a Mock Object

Creating a mock object is straightforward. Once imported, you can instantiate a mock object like this:

 

Now, my_mock is a mock object that you can configure to simulate the behavior of a real object.

 

Step 3: Setting Return Values

The Mock library provides various ways to configure mock objects and control their behavior. For instance, you can specify what a method should return when called:

my_mock.some_method.return_value="Hello, World!"
print(my_mock.some_method())

 

Output:

 

Step 4: Setting Side Effects

Side effects are additional actions or behaviors triggered when a method of a mock object is called, such as raising exceptions or executing functions. Besides return values, you can also define attributes or specify side effects like this:

def raise_exception():
    raise ValueError("An error occurred")

my_mock.some_method.side_effect = raise_exception

# This will raise a ValueError
try:
    my_mock.some_method()
except ValueError as e:
    print(e)  

 

Output:

 

In this example, ValueError raises whenever some_method() is called.

 

Step 5: Asserting Calls

Verifying the method calls is crucial for thorough testing. You can use assertions to specify whether a method was called, when, and with what arguments.

my_mock.calculate_length('foo', 'bar')
my_mock.calculate_length.assert_called()
my_mock.calculate_length.assert_called_once()
my_mock.calculate_length.assert_called_with('foo', 'bar')
my_mock.calculate_length.assert_called_once_with('foo', 'bar')

 

  • assert_called(): Returns True if calculate_length was called at least once
  • assert_called_once(): Returns True if calculate_length was called exactly once
  • assert_called_with('foo', 'bar'): Returns True if calculate_length was called with the same arguments
  • assert_called_once_with('foo', 'bar'): Returns True if calculate_length was called exactly once with the same arguments

If any of these assertions fail on the mock object, an AssertionError will be raised, indicating that the expected behavior did not match the actual behavior of the mock.

 

Step 6: Using Patch

The patch function allows you to replace real objects with mock objects during tests. As discussed earlier, this is particularly useful for simulating third-party libraries or APIs, ensuring your tests remain isolated from actual implementations. To demonstrate patching, consider the following example function that fetches data from the URL.

# my_module.py
import requests

def fetch_data(url):
    response = requests.get(url)
    return response.json()

 

You can avoid making real HTTP requests by patching the ‘requests.get’ like this:

# test_my_module.py
import unittest
from unittest.mock import patch
import my_module

class TestFetchData(unittest.TestCase):
    @patch('my_module.requests.get')

    def test_fetch_data(self, mock_get):
        # Set up the mock to return a specific response
        mock_get.return_value.json.return_value = 'key': 'value'
       
        # Call the function to test
        result = my_module.fetch_data('http://example.com')
       
        # Check the result
        self.assertEqual(result, 'key': 'value')
       
        # Verify that requests.get was called correctly
        mock_get.assert_called_once_with('http://example.com')

if __name__ == '__main__':
    unittest.main()

 

The patch decorator is added just above the test_fetch_data function to replace the requests.get function with a mock.

 

Step 7: Mocking Classes

You can mock entire classes and their methods to simulate interactions between objects. For instance, you can mock a database class to test your application’s interaction with the database without the need to set up a real database connection like this:

# database.py
class Database:
    def connect(self):
        pass

    def save_user(self, user):
        pass

    def get_user(self, user_id):
        pass


# test_database.py
from unittest.mock import Mock

# Creating a mock database object
mock_db = Mock(spec=Database)

# Simulating method calls
mock_db.connect()
mock_db.save_user("id": 1, "name": "Alice")
mock_db.get_user(1)

# Verifying that the methods were called
mock_db.connect.assert_called_once()
mock_db.save_user.assert_called_once_with("id": 1, "name": "Alice")
mock_db.get_user.assert_called_once_with(1)

 

Wrapping Up

 
That’s it for today’s article on unittest.mock, a powerful library for testing in Python. It enables developers to test code, ensuring smooth interactions between objects. With advanced features like specifying side effects, asserting calls, mocking classes, and using context managers, testing various scenarios becomes easier. Start using mocks in your tests today to ensure higher-quality code and smoother deployments.

 
 

Kanwal Mehreen Kanwal is a machine learning engineer and a technical writer with a profound passion for data science and the intersection of AI with medicine. She co-authored the ebook “Maximizing Productivity with ChatGPT”. As a Google Generation Scholar 2022 for APAC, she champions diversity and academic excellence. She’s also recognized as a Teradata Diversity in Tech Scholar, Mitacs Globalink Research Scholar, and Harvard WeCode Scholar. Kanwal is an ardent advocate for change, having founded FEMCodes to empower women in STEM fields.

Recent Articles

Related Stories

Leave A Reply

Please enter your comment!
Please enter your name here