Mastering Advanced Spies & Call Inspection

by Admin 43 views
🎓 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)