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

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:

my_mock = Mock()

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:

Hello, World!

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:

An error occurred

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.

More On This Topic

  • Introducing the Testing Library for Natural Language Processing
  • Hypothesis Testing and A/B Testing
  • A Step-by-Step Guide to Web Scraping with Python and Beautiful Soup
  • Free Python Resources That Can Help You Become a Pro
  • A Guide On How To Become A Data Scientist (Step By Step Approach)
  • How To Structure a Data Science Project: A Step-by-Step Guide
Follow us on Twitter, Facebook
0 0 votes
Article Rating
Subscribe
Notify of
guest
0 comments
Oldest
New Most Voted
Inline Feedbacks
View all comments

Latest stories

You might also like...