64.4 Asserting Call Counts and Arguments
Right, so you’ve mocked out a function. You’ve set it up to return a specific value. Your test passes. High fives all around. But wait—did your code under test actually call that mock? And if it did, how many times? With what arguments? This is where we move from just checking state to verifying behavior, and it’s a crucial step up in your testing game. Let’s be honest: if you’re not verifying these interactions, you’re only testing half the story, and the other half is probably hiding a bug.
Think of it like this: you asked your friend to order a pizza (the function call) and they handed you a pizza (the return value). Great! But did they actually call the pizzeria, or did they just pull one out of the fridge? Verifying the call count and arguments is like checking their phone’s call history to see if they dialed the right number and actually placed the order.
The Basic Tools: called and call_count
Every decent mocking library gives you a way to check if a mock was called at all and how many times. In unittest.mock, it’s dead simple.
from unittest.mock import Mock
# Let's mock a function that sends a notification
notifier = Mock()
# Our system under test might call it... or not.
def process_order(user_id):
if user_id > 1000:
notifier.send(f"Order for user {user_id} processed")
# Test case 1: It should be called for a valid user
process_order(1001)
assert notifier.called # Did it get called? True
assert notifier.call_count == 1 # How many times? 1
# Test case 2: It should NOT be called for an invalid user
process_order(999)
# Our previous assertions would fail now because call_count is still 1!
# We need to reset or set up a fresh mock for each test.
The pitfall here is obvious: mocks are stateful. If you don’t reset them between tests (or better yet, create new ones in a setUp method), your call_count will be cumulative, and your tests will be hopelessly interdependent and broken. Don’t do that. Always start fresh.
Digging Deeper: Checking the Arguments
Knowing a function was called is good. Knowing it was called correctly is everything. This is where you catch those off-by-one errors, incorrect string formats, and None values where you expected a real object.
# Let's check what arguments our mock was called with
notifier = Mock()
process_order(1001)
# The most basic check: was it called with exactly these arguments?
notifier.send.assert_called_with("Order for user 1001 processed")
# This will raise an AssertionError if the call doesn't match exactly.
# And I mean *exactly*. Same number of args, same values, same order.
Beyond Exact Matches: call_args and call_args_list
Sometimes, you need more flexibility. Maybe you only care about the first argument, or you need to check that a particular keyword argument was present. This is where digging into the call details comes in.
notifier = Mock()
notifier.send("hello", urgent=True)
notifier.send("world")
# Get the tuple of (args, kwargs) for the most recent call
last_call = notifier.send.call_args
print(last_call) # prints: call('world'), {}
# Or get a list of all calls
all_calls = notifier.send.call_args_list
print(all_calls) # prints: [call('hello', urgent=True), call('world')]
# Now you can make more nuanced assertions
assert notifier.send.call_args_list[0][1]['urgent'] is True
# But frankly, that's a bit ugly. Let's use a better pattern.
The Elegant Way: Using any_order and Flexible Assertions
Trying to index into call_args_list manually is a recipe for brittle tests. Instead, use the helper methods designed for this. Need to check a function was called multiple times with specific arguments, in any order?
from unittest.mock import call
notifier = Mock()
notifier.send("hello")
notifier.send("goodbye")
# Check it was called with both, in any order
notifier.send.assert_has_calls([call("goodbye"), call("hello")], any_order=True)
This is incredibly powerful for testing functions that might make several related calls where the sequence isn’t important, but the totality is.
The Giant Footgun: Mutable Arguments
Here’s a classic “I’ve been in the trenches” moment for you. Let’s say your mock is called with a list.
my_mock = Mock()
my_list = [1, 2, 3]
my_mock.process(my_list)
# Later, your code under test (or some other part of the test) modifies the list.
my_list.append(4)
# Now, what does the mock have recorded?
print(my_mock.call_args) # prints: call([1, 2, 3, 4])
Yikes. The mock doesn’t store a copy of the arguments at the time of the call; it stores a reference to the very same mutable object. If that object changes later, your assertion checks the current state, not the state at the time of the call. This can lead to nightmarish, flaky tests. The best practice? In your tests, if you’re passing mutable data to a function you’re going to verify, either don’t mutate it afterwards or use the call_args to make a deep copy of the arguments for your assertions immediately after calling the system under test.
Verifying calls isn’t just pedantry; it’s about ensuring the contracts between your objects are upheld. Do it wisely, and you’ll catch a whole class of bugs that simply checking return values will miss.