Skip to main content

Python mocking frameworks

· 7 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

Mocking becomes particularly valuable when testing code that interacts with complex frameworks and libraries. While the core concepts of unittest.mock remain the same, the specific objects and functions you need to patch can vary. This article will provide practical, in-depth examples of how to apply mocking to three common Python ecosystems: web frameworks (Django/Flask), asynchronous programming (asyncio), and data science (pandas).

Mocking in Web Frameworks (Django/Flask)

Web applications frequently make calls to external APIs, databases, and third-party services. Mocking is essential here for isolating your view logic and making tests fast and reliable. The most common use cases involve mocking the request object and database queries.

Mocking the Request Object

When testing a view function, you often need to simulate a client's request. Instead of using a test client to send a real HTTP request, you can simply create and pass in a mock request object.

Example: Mocking a Flask request

my_app/views.py

from flask import request

def get_user_agent():
return request.headers.get('User-Agent')

tests/test_views.py

import unittest
from unittest.mock import MagicMock
from my_app.views import get_user_agent

class TestViews(unittest.TestCase):
def test_get_user_agent(self):
# Create a mock for the `request` object
mock_request = MagicMock()
mock_request.headers.get.return_value = 'Mozilla/5.0'

# Patch the `request` object itself
with unittest.mock.patch('my_app.views.request', mock_request):
user_agent = get_user_agent()
self.assertEqual(user_agent, 'Mozilla/5.0')
mock_request.headers.get.assert_called_once_with('User-Agent')

By using patch as a context manager, we replace the request object imported by views.py with our mock, allowing us to control its behavior precisely.


Mocking Asynchronous Code with AsyncMock

Asynchronous programming with asyncio presents unique testing challenges. You cannot simply use a standard mock object to replace an async function, as it would return a regular mock instead of an awaitable one. The AsyncMock class from unittest.mock solves this problem by creating a mock that can be awaited [1].

Example: Mocking an async API call

my_app/async_api.py

import aiohttp

async def fetch_data(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.json()

tests/test_async_api.py

import unittest
import asyncio
from unittest.mock import AsyncMock, patch
import aiohttp
from my_app.async_api import fetch_data

class TestAsyncAPI(unittest.TestCase):

@patch('my_app.async_api.aiohttp.ClientSession')
def test_fetch_data_success(self, mock_session_class):
# The key is to mock the entire chain of async calls
mock_session_instance = mock_session_class.return_value
mock_get_context = mock_session_instance.get.return_value.__aenter__.return_value
mock_get_context.json.return_value = {'data': 'mocked_data'}

async def run_test():
result = await fetch_data('http://example.com')
self.assertEqual(result, {'data': 'mocked_data'})

asyncio.run(run_test())

This example is complex because asyncio's context managers (async with) and awaited calls (await) must all be mocked. AsyncMock correctly handles the __aenter__ and __aexit__ methods of the context manager, as well as the awaitable json() method [2].


Mocking Data Science and Pandas

In data science, a test often needs to run on a large dataset. Loading a 10GB CSV file for every test is impractical. Mocking allows you to test your data processing logic without loading any data into memory [3].

Example: Mocking a Pandas DataFrame read

data_processor.py

import pandas as pd

def process_data(file_path):
df = pd.read_csv(file_path)
df['total_sales'] = df['quantity'] * df['price']
return df

tests/test_data_processor.py

import unittest
from unittest.mock import patch, MagicMock
import pandas as pd
from io import StringIO
from data_processor import process_data

class TestDataProcessor(unittest.TestCase):

@patch('data_processor.pd.read_csv')
def test_process_data_correctly(self, mock_read_csv):
# Create a mock DataFrame that the function will "read"
mock_df = pd.DataFrame({
'quantity': [10, 20],
'price': [5.0, 10.0]
})
mock_read_csv.return_value = mock_df

# Now, the function will use our mock DataFrame
processed_df = process_data('sales.csv')

# We can assert that the correct columns were added
self.assertIn('total_sales', processed_df.columns)
self.assertEqual(list(processed_df['total_sales']), [50.0, 200.0])

This is a clean and effective way to test data transformation logic. By mocking pd.read_csv, you can inject a small, controlled DataFrame directly into your function, ensuring the test is fast and focused only on the logic of your process_data function.

Sources

  1. Python asyncio.run() mocking
  2. Testing async code with unittest.mock
  3. Mocking and Pandas Testing
  4. The Ultimate Guide to Mocking in Python

This video provides a great visual explanation of mocking in Flask applications. How To Unit Test Flask Applications with Mocking