Mock External Dependencies in Tests
Mock external dependencies in tests: learn to isolate your Python code by replacing external services and APIs with controlled mock objects. This hands-on tutorial covers the core concept, step-by-step implementation, comparison with alternatives, troubleshooting edge cases, and next steps.
Focus: mock external dependencies in tests
When your Python application calls an external API, database, or third-party service, your tests become slow, fragile, and dependent on network availability. Hitting real endpoints during testing leads to failures that have nothing to do with your code — flaky tests that erode trust in your suite. Mocking external dependencies is the solution: you replace those remote calls with controlled, predictable stand-ins, so your tests run fast and focus on your logic, not the outside world.
The problem this lesson solves
Integration-style tests that reach out to real external services are costly. A single test might take seconds when it should take milliseconds. Worse, if the external service goes down, your tests break — even though your code is perfect. This creates false positives (or negatives) and makes CI pipelines unpredictable. Consider a function that fetches user data from a REST API — without mocking, every test run makes a real HTTP request:
import requests
def get_user(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
return response.json()
If the API is unreachable or returns a 500, the test fails, not because get_user is buggy, but because the network or backend has an issue. Mocking external dependencies in tests solves this by letting you simulate the API's behavior — good responses, timeouts, errors — without touching the real endpoint.
Core concept / mental model
Think of mocking like a stunt double in a movie. The actor (your production code) hands off dangerous or complex scenes to a professional stand-in (the mock). When you test, you swap the real external dependency with a mock object that behaves exactly as you instruct it to — it doesn't perform the actual operation, but it returns the right data, raises the right exceptions, or logs the right calls.
In Python, the built-in unittest.mock module provides two main tools:
- Mock — a flexible, all-purpose stand-in for any object.
- patch — a context manager/decorator that temporarily replaces an attribute (like a function or class) in a module or object.
Pro tip: Use
patchwhen you need to replace a specific import or object (e.g.,requests.get) andMagicMockwhen your mock needs to support magic methods like__len__or__iter__.
How it works step by step
Let's break down the mechanism of mocking an external HTTP call:
1. Identify the target to mock
Locate the exact import path in your code that performs the external call. For requests.get used inside a module called myapp.service, the full path is myapp.service.requests.get.
2. Choose the mock strategy
- Decorator: Use
@patch('myapp.service.requests.get')to automatically pass aMockobject as an argument to your test function. - Context manager: Use
with patch('myapp.service.requests.get') as mock_get:for finer control inside the test body. - Manual mock: Create a
Mockinstance and assign it directly — useful when patching objects you own.
3. Configure the mock's return value
Tell the mock what to return when called. For mock_get.return_value, you can set:
- A simple value (e.g., a dict)
- A complex object that mimics a real response (e.g., a Mock with a .json() method)
- Side effects for sequential calls (list of return values or exceptions)
4. Run the test and assert
Call your production function, then assert that the mock was called correctly and that your code handles the mocked response as expected.
Hands-on walkthrough
Now let's apply this to the get_user function from earlier. We'll write a test that mocks requests.get and verifies the function returns the parsed JSON.
Example 1: Basic mock of requests.get
# myapp/service.py
import requests
def get_user(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
return response.json()
# tests/test_service.py
from unittest.mock import patch, Mock
from myapp.service import get_user
@patch('myapp.service.requests.get')
def test_get_user_returns_data(mock_get):
# Simulate a successful response
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"id": 1, "name": "Alice"}
mock_get.return_value = mock_response
result = get_user(1)
assert result == {"id": 1, "name": "Alice"}
mock_get.assert_called_once_with("https://api.example.com/users/1")
Expected output: The test passes. The real requests.get is never called.
Example 2: Mock with side effects (error scenario)
@patch('myapp.service.requests.get')
def test_get_user_raises_on_http_error(mock_get):
import requests
mock_response = Mock()
mock_response.status_code = 404
mock_response.raise_for_status.side_effect = requests.HTTPError("Not Found")
mock_get.return_value = mock_response
with pytest.raises(requests.HTTPError):
get_user(999)
Expected output: The test verifies that get_user propagates the HTTP error from requests.get.
Example 3: Mock a database connection
# myapp/db.py
import psycopg2
def fetch_users():
conn = psycopg2.connect("dbname=test")
cur = conn.cursor()
cur.execute("SELECT * FROM users")
return cur.fetchall()
@patch('myapp.db.psycopg2.connect')
def test_fetch_users_returns_rows(mock_connect):
mock_conn = Mock()
mock_cursor = Mock()
mock_cursor.fetchall.return_value = [(1, "Alice"), (2, "Bob")]
mock_conn.cursor.return_value = mock_cursor
mock_connect.return_value = mock_conn
result = fetch_users()
assert result == [(1, "Alice"), (2, "Bob")]
mock_connect.assert_called_once_with("dbname=test")
Expected output: The test passes and never opens a real database connection.
Pro tip: When mocking complex objects like database connections or HTTP clients, create helper factory functions to build your mock responses — keeps tests clean and DRY.
Compare options / when to choose what
| Approach | Best for | Limitation |
|---|---|---|
unittest.mock.patch |
Most external dependencies (HTTP, DB, filesystem) | Requires exact import path — fragile under refactoring |
pytest-mock (plugin) |
Easier syntax with mocker fixture, automatic cleanup |
Adds dependency; no benefit over built-in for simple cases |
responses library |
Mocking HTTP requests with pre-recorded payloads | Only for HTTP; not suitable for databases or other services |
| Dependency injection | Clean architecture, test doubles without monkey-patching | More initial setup; overkill for small projects |
When to choose what:
- For a quick, isolated unit test: use patch.
- If you love pytest fixtures and want mocker.spy and mocker.stopall: install pytest-mock.
- To test how your code handles specific HTTP status codes or bodies: responses gives you fine-grained control.
- In a large, modular codebase: prefer inversion of control and pass mock objects explicitly.
Troubleshooting & edge cases
Mock not being called (test passes but shouldn't)
- Issue: You patched the wrong path. Remember: use the path where the name is looked up, not where it's defined.
- Correct:
@patch('myapp.service.requests.get') - Wrong:
@patch('requests.get')
Mock object is called but not returning what you expect
- Issue: You set
return_valueon the mock, but the mock itself is called with arguments and returns a new mock. Solution: setreturn_valueon the call result, not the mock directly.python mock_get.return_value = mock_response # Correct
Multiple calls to the same mock
- Issue: Your production code calls the same external function more than once. Use
side_effectwith a list of return values or exceptions.python mock_get.side_effect = [response1, response2, requests.HTTPError("Timeout")]
Timeouts and delays
- Edge case: Your mock needs to simulate a slow network. Set
side_effectto a function that sleeps before returning:python import time def slow_response(*args, **kwargs): time.sleep(2) return Mock(status_code=200, json=lambda: {"data": "slow"}) mock_get.side_effect = slow_response
Pro tip: Always verify the mock was called with the correct arguments using
assert_called_once_with(...)— it catches off-by-one errors in URL construction or parameter passing.
What you learned & what's next
You now understand how to mock external dependencies in tests: isolate your code by replacing real services with unittest.mock.Mock objects. You practiced patching requests.get and a database connector, compared mocking strategies with tables, and learned to troubleshoot common pitfalls like wrong patch paths or missing return values.
Key takeaways:
- Mocking decouples tests from slow, unreliable external services.
- Always patch the name as it's imported in your module, not where it's defined.
- Use return_value for simple responses and side_effect for sequences or exceptions.
- Combine mocking with assertions like assert_called_once_with to verify behavior.
- Consider tools like pytest-mock or responses for added convenience, but unittest.mock works everywhere.
Next lesson: Mock external APIs with pytest-mock — you'll learn how pytest fixtures make mocking even cleaner, plus how to assert that mocks were never called (or called exactly N times).
Now go ahead and complete the hands-on exercise below to cement your skills.
Practice recap
Mini exercise: Refactor the following test so it mocks the external HTTP call: def fetch_weather(city): return requests.get(f'https://api.weather.com/{city}').json(). Write a test that asserts the function returns the mocked JSON when requests.get is patched. Then add a test that verifies the function raises requests.HTTPError when the API returns 500. Check your mocks with assert_called_once_with.
Common mistakes
- Patching the wrong module path — always patch the name as it's imported in your module, not where it's defined in the library.
- Forgetting to set
return_valueon the mock's return object (e.g., forgettingmock_response.json.return_value = ...), causing the test to get another Mock instead of real data. - Using
side_effectwhen you intendedreturn_value, leading to unexpected exceptions or misbehavior. - Not cleaning up mocks between tests — use
patchas a context manager or decorator to avoid state leaks.
Variations
- Use
pytest-mockplugin for cleaner fixture-based mocking andmocker.spy()for verifying calls. - Replace mocking with dependency injection — pass a stub or fake object directly to your function, eliminating the need for patching.
- Use
responseslibrary (for HTTP) orfreezegun(for time) when you need pre-recorded interactions and deterministic control.
Real-world use cases
- E-commerce checkout service mock: Replace Stripe API calls in tests to validate payment processing without hitting production endpoints.
- Data pipeline test: Mock an S3 bucket read to verify transformation logic runs correctly before the pipeline touches real cloud storage.
- Microservice integration: Mock a UserProfile service response to test how your notification service handles different user tiers.
Key takeaways
- Mocking external dependencies isolates tests from network, databases, and flaky services, making them fast and reliable.
- Always patch the import path as used in the calling module —
myapp.service.requests.get, notrequests.get. - Use
return_valuefor single results andside_effectfor sequential or exception-based behaviors. - Assert mock call counts and arguments with
assert_called_once_with(...)to verify correct usage. - Choose
unittest.mockfor built-in simplicity,pytest-mockfor pytest conveniences, or dependency injection for large codebases. - Mocks can simulate errors, timeouts, and multi-step interactions — they're not just for happy paths.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.