Mastering Advanced Spies & Call Inspection
🎯 Learning Objectives
Hey guys! Today, we're diving deep into the world of testing with advanced spies. We'll learn some super cool techniques to check how and how many times a function gets called, and we'll be able to peek at all the juicy details like the arguments used, even the return values! This is going to be fun, I promise. Plus, we'll get to see real-world examples, like using spies with API clients, workflows, pipelines, and internal services, so get ready to level up your testing game. Let's get started. We'll be focusing on a few key areas:
- Using advanced spies to verify how and how many times a function is called.
- Analyzing captured arguments, including
args,kwargs, and return values. - Identifying scenarios where a mock isn't enough and a spy is essential.
- Applying these techniques to common patterns found in real systems, such as API clients, workflows, pipelines, and internal services.
Why This Matters
Why should you care about this? Well, understanding spies will seriously improve your testing abilities. It's about making sure your code does exactly what it's supposed to. Using spies can help you in a lot of areas, specifically when you need to confirm that certain functions are called, the order they are called in, and the arguments they're called with. This is crucial for verifying the behavior of your code without having to completely replace or mock out the underlying logic. It's a powerful tool, trust me.
📚 Concepts to Practice
Before we dive in, let's make sure we're on the same page. Here are the main concepts we'll be playing with today:
- Correctly using
mocker.spy(obj, "method")- this is the core of spying! - Inspecting calls using:
.call_count- how many times was the function called?.called- was the function called at all?.call_args- what were the arguments of the last call?.call_args_list- a list of all the calls and their arguments.
- Verifying positional and keyword parameters – make sure the right stuff is passed in!
- Verifying the order of calls - did things happen in the right sequence?
- Spying on functions that should execute – unlike mocks, spies let the real function run.
✅ Success Criteria
Alright, so how will you know you've nailed this? Here’s what you should have by the end of this exercise:
- Implemented code (module + logic that invokes other methods).
- Tests that pass using spies accurately.
- Updated README/documentation explaining how and when to use spies.
Basically, you should have code that uses spies effectively and know when and how to use them. It's that simple, well not entirely, but you will get there!
🔍 Implementation Approach
Okay, let's break this down step by step to make sure you truly understand this stuff.
1️⃣ What is a “spy”?
So, what's a spy? A spy doesn't replace the function; it watches it as it runs. Think of it like a secret agent. The function performs its duties as usual, and the spy is just there to observe. This is what sets it apart from a mock.
Here’s a simple table to show the differences between a mock and a spy. Pay close attention because this distinction is really important.
| Feature | Mock | Spy |
|---|---|---|
| Executes the real function | ❌ | ✔ |
| Allows you to see how many times it was called | ✔ | ✔ |
| Allows inspection of arguments | ✔ | ✔ |
| Allows simulating responses | ✔ | ❌ (except in advanced combinations) |
| Useful in cases where the function executes real logic that we want to keep | ❌ | ✔ |
So, in short, a spy lets the real function run but keeps an eye on it. A mock, on the other hand, is a substitute. They both help you with testing, but they're used in different situations. Got it?
2️⃣ Practical Case of the Day
Now, let's look at the actual code and how to apply these concepts. You're going to create a module with a class. This class will look something like this:
class Notificador:
def validar_mensaje(self, msg: str) -> bool:
return bool(msg and msg.strip())
def enviar_mensaje(self, msg: str) -> dict:
return {"ok": True, "msg": msg}
def procesar(self, msg: str) -> dict:
# Paso 1: validar
if not self.validar_mensaje(msg):
return {"ok": False, "error": "mensaje_vacio"}
# Paso 2: enviar
return self.enviar_mensaje(msg)
Inside this class, you'll have a method that depends on another internal method. We're going to be spying on validar_mensaje() or enviar_mensaje() to confirm a few things:
- How many times was the function called?
- What arguments were used?
- In what order were the calls made?
This will give you hands-on experience in using spies, which will solidify your understanding.
3️⃣ Base Code
Here is the code base you'll need to work with. Take a look and get ready to start spying!
# src/workflow/notificador.py
class Notificador:
def validar_mensaje(self, msg: str) -> bool:
return bool(msg and msg.strip())
def enviar_mensaje(self, msg: str) -> dict:
return {"ok": True, "msg": msg}
def procesar(self, msg: str) -> dict:
# Step 1: validate
if not self.validar_mensaje(msg):
return {"ok": False, "error": "mensaje_vacio"}
# Step 2: send
return self.enviar_mensaje(msg)