πŸ§ͺ Testing Methods *Before* they are Methods β€” The `patch_to` Hack

Sometimes you want to test methods of a class individually β€” but that gets tricky if those methods depend on the full class context (instantiation, parameters, etc.).

So how do you test a method before the class is ready β€” or before the method is even part of the class?


:prohibited: One way β€” But It’s a Bit Clunky

from dataclasses import dataclass
from fastcore.basics import patch_to
from types import SimpleNamespace
from fastcore.test import test_eq

@dataclass
class Animal:
    name: str
    sound: str

# Define a raw FUNCTION
def speak_raw(self: Animal) -> str:
    return f"{self.name} says {self.sound}"

# βœ… Test the raw FUNCTION with a dummy object
dummy = SimpleNamespace(name="Lion", sound="Roar")
test_eq(speak_raw(dummy), "Lion says Roar")

# πŸ”§ Patch it into the class
@patch_to(Animal)
def speak(self) -> str:
    return speak_raw(self)

# βœ… Use it like a regular method
a = Animal(name="Dog", sound="Woof")
test_eq(a.speak(), "Dog says Woof")
print(a.speak())  # "Dog says Woof"

But there’s one awkward bit: you need two different names (speak_raw, speak) because @patch_to replaces the original name in the global namespace. Re-using the same name would lead to errors or infinite recursion (I hope I got the explanation right).


:light_bulb: The Cleaner Way β€” Test First, Then Patch

Today I had an idea.

Here’s the key insight: you can define your method as a regular function, test it, and only later attach it using patch_to β€” all while keeping the same name.


:white_check_mark: 0. Define Your Class

from dataclasses import dataclass

@dataclass
class Animal:
    name: str
    sound: str

:white_check_mark: 1. Define the Method as a Function

def speak(self: Animal) -> str:
    return f"{self.name} says {self.sound}"

:white_check_mark: 2. Test the Function with a Dummy Object

from types import SimpleNamespace
from fastcore.test import test_eq

dummy = SimpleNamespace(name="Lion", sound="Roar")
test_eq(speak(dummy), "Lion says Roar")

:white_check_mark: 3. Attach the Function as a Method Using patch_to

from fastcore.basics import patch_to

patch_to(Animal)(speak)  # ← This is the magic moment

:white_check_mark: 4. Use It as a Method

a = Animal(name="Dog", sound="Woof")
test_eq(a.speak(), "Dog says Woof")
print(a.speak())  # "Dog says Woof"

Maybe you knew this all along. I haven’t seen it before and I wish I had.


:puzzle_piece: Full Example (for easy copy-paste / testing)

#|export
from dataclasses import dataclass
from types import SimpleNamespace
from fastcore.test import test_eq
from fastcore.basics import patch_to

@dataclass
class Animal:
    name: str
    sound: str

# Define the method as a testable function
def speak(self: Animal) -> str:
    return f"{self.name} says {self.sound}"

# βœ… Test the function
dummy = SimpleNamespace(name="Lion", sound="Roar")
test_eq(speak(dummy), "Lion says Roar")

# πŸ”§ Patch it into the class
patch_to(Animal)(speak)

# βœ… Now use it like a method
a = Animal(name="Dog", sound="Woof")
test_eq(a.speak(), "Dog says Woof")
print(a.speak())

:brain: Why This Matters

  • :white_check_mark: Enables test-first development, even with instance-bound logic.
  • :white_check_mark: Reduces coupling to class state until the function is stable.
  • :white_check_mark: Avoids namespace clobbering or recursion bugs with @patch_to.
  • :white_check_mark: Perfectly suited for nbdev or interactive notebook workflows.