Python Mocking and Fixtures: Test Code with External Dependencies
Before car companies test a new vehicle in a real crash, they use a crash test dummy. The dummy is a stand-in for a real person — it behaves similarly enough to be useful, but nobody gets hurt.
Mocking works the same way in code. When your function talks to a database, sends an email, or calls an API, you don't want your tests to actually do those things. Instead, you create a mock — a fake stand-in that pretends to be the real thing.
In this tutorial, you'll learn how to create mock objects, replace real dependencies with fakes, and set up test fixtures so each test starts with a clean slate. We'll use patterns that work right here in the browser.
What Is Mocking and When Do You Need It?
Imagine you write a function that checks whether a user exists in a database. To test it, you'd need a real database with real data. That's slow, fragile, and complicated.
Mocking lets you replace the database with a fake object that returns whatever data you want. Your function can't tell the difference — it works exactly the same way, but your test runs instantly and doesn't need any external setup.
You need mocking when your code depends on things you can't control:
How Do You Create a Mock Object?
A mock object is simply a fake version of something real. The simplest mock is a class with the same methods as the real thing, but with hardcoded return values.
The key insight is that greet_user doesn't care whether it gets a RealDatabase or a MockDatabase. It just calls .get_user(). This is called dependency injection — you pass the dependency in rather than hardcoding it.
How Do You Build a Flexible Mock?
Simple mocks with hardcoded data work, but sometimes you need more power. A flexible mock can track how many times it was called, what arguments it received, and return different values for different inputs.
This mock lets you verify not just that your code produces the right output, but that it calls its dependencies correctly. You can check: "Did it call the API exactly once? Did it pass the right URL?"
How Do You Replace Real Dependencies in Tests?
The most common mocking pattern is replacing a real dependency with a fake one for the duration of a test. There are two main approaches: passing the mock as a parameter (injection) or temporarily swapping it out.
The second approach is useful when you can't change the function signature — you temporarily swap out a module-level variable:
def get_weather():
response = requests.get('https://api.weather.com')
return response.json()['temp']def get_weather(api_client):
response = api_client.get('https://api.weather.com')
return response.json()['temp']What Are Test Fixtures and How Do Setup/Teardown Work?
A fixture is any setup work that your tests need before they run. Think of it like setting the table before dinner — you prepare everything, eat (run the test), then clean up afterward.
Common fixtures include: creating test data, initializing objects, setting up mock databases, or configuring temporary state. The key rule is that each test should start with a fresh fixture, so tests don't affect each other.
Notice how each test calls make_cart() to get its own fresh cart. If one test modifies the cart, it doesn't affect the others. This is called test isolation and it's crucial for reliable tests.
For more complex scenarios, you can use setup and teardown functions — code that runs before and after each test:
Practice Exercises
Create a MockEmailService class with a send_email(to, subject, body) method that stores sent emails in a list called sent_emails (as tuples). Then write a notify_user function that uses the service to send a welcome email. Test that calling notify_user results in exactly one email with the correct recipient. Print Mock email test passed! at the end.
Read this code carefully. A mock logger tracks method calls. Predict exactly what gets printed.
The get_greeting function uses get_current_hour() to decide the greeting. Monkey-patch get_current_hour to test all three cases: morning (hour=9), afternoon (hour=14), and evening (hour=20). Use try/finally to restore the original. Print each greeting on its own line, then print Time mock test passed!.
Create a make_inventory fixture function that returns a dictionary with {'apple': 10, 'banana': 5, 'cherry': 0}. Write three test functions that each call make_inventory() for a fresh copy: test_in_stock (apple count > 0), test_out_of_stock (cherry count == 0), and test_add_stock (add 3 bananas, then banana count == 8). Run all three and print All inventory tests passed!.
This test is supposed to verify that calculate_total applies a 10% discount. But the mock is set up incorrectly — it's not returning the data the function expects. Fix the MockPricingService so the test passes and prints Discount test passed!.