The Problem

Mock Objects

i
import time

def elapsed(since):
    return time.time() - since

def mock_time():
    return 200

def test_elapsed():
    time.time = mock_time
    assert elapsed(50) == 150

Callable

i
class Adder:
    def __init__(self, value):
        self.value = value

    def __call__(self, arg):
        return arg + self.value

add_3 = Adder(3)
result = add_3(8)
print(f"add_3(8): {result}")
i
add_3(8): 11

A Generic Replacer

i
class Fake:
    def __init__(self, func=None, value=None):
        self.calls = []
        self.func = func
        self.value = value

    def __call__(self, *args, **kwargs):
        self.calls.append([args, kwargs])
        if self.func is not None:
            return self.func(*args, **kwargs)
        return self.value
i
def fakeit(name, func=None, value=None):
    assert name in globals()
    fake = Fake(func, value)
    globals()[name] = fake
    return fake

Replacement in Action

i
def adder(a, b):
    return a + b

def test_with_real_function():
    assert adder(2, 3) == 5
i
def test_with_fixed_return_value():
    fakeit("adder", value=99)
    assert adder(2, 3) == 99

Replacement in Action

Timeline of mock operation
Figure 1: Timeline of mock operation.

But Wait, There's More

i
def test_fake_records_calls():
    fake = fakeit("adder", value=99)
    assert adder(2, 3) == 99
    assert adder(3, 4) == 99
    assert adder.calls == [[(2, 3), {}], [(3, 4), {}]]
i
def test_fake_calculates_result():
    fakeit("adder", func=lambda left, right: 10 * left + right)
    assert adder(2, 3) == 23

Protocols

Operation

i
with C(args) as name:
    do things
  1. Call C's constructor to create an object.
  2. Call that object's __enter__ method and assign the result to name.
  3. Run the code inside the with block.
  4. Call name.__exit__() when the block finishes.

  5. __enter__ doesn't need extra arguments

    • Use the object's constructor
  6. Python calls __exit__ with three values for error handling

Mock With Context

i
class ContextFake(Fake):
    def __init__(self, name, func=None, value=None):
        super().__init__(func, value)
        self.name = name
        self.original = None

    def __enter__(self):
        assert self.name in globals()
        self.original = globals()[self.name]
        globals()[self.name] = self
        return self

    def __exit__(self, exc_type, exc_value, exc_traceback):
        globals()[self.name] = self.original

Wrapping Functions

i
def original(value):
    print(f"original: {value}")

def logging(value):
    print("before call")
    original(value)
    print("after call")

original = logging
original("example")
i
before call
before call
before call

Capture the Original

i
def original(value):
    print(f"original: {value}")

def logging(func):
    def _inner(value):
        print("before call")
        func(value)
        print("after call")
    return _inner

original = logging(original)
original("example")
i
before call
original: example
after call

Parameters

i
def original(value):
    print(f"original: {value}")

def logging(func, label):
    def _inner(value):
        print(f"++ {label}")
        func(value)
        print(f"-- {label}")
    return _inner

original = logging(original, "call")
original("example")
i
++ call
original: example
-- call

Decorators

i
def wrap(func):
    def _inner(*args):
        print("before call")
        func(*args)
        print("after call")
    return _inner

@wrap
def original(message):
    print(f"original: {message}")

original("example")
i
before call
original: example
after call

Decorator Parameters

i
def wrap(label):                  # function returning a decorator
    def _decorate(func):          # the decorator Python will apply
        def _inner(*args):        # the wrapped function
            print(f"++ {label}")  # 'label' is visible because
            func(*args)           # …it's captured in the closure
            print(f"-- {label}")  # …of '_decorate'
        return _inner
    return _decorate

@wrap("wrapping")                 # call 'wrap' to get a decorator
def original(message):            # decorator applied here
    print(f"original: {message}")

original("example")
i
++ wrapping
original: example
-- wrapping

Design Flaw

i
def decorator(func, label):
    def _inner(arg):
        print(f"entering {label}")
        func(arg)
    return _inner

@decorator("message")
def double(x):           # equivalent to
    return 2 * x         # double = decorator(double, "message")

Iteration

Loop Over a List of Strings

i
class BetterIterator:
    def __init__(self, text):
        self._text = text[:]

    def __iter__(self):
        return BetterCursor(self._text)
i
class BetterCursor:
    def __init__(self, text):
        self._text = text
        self._row = 0
        self._col = -1

    def __next__(self):
        self._advance()
        if self._row == len(self._text):
            raise StopIteration
        return self._text[self._row][self._col]

# ...6 lines not shown...

Iterator in Action

i
def test_naive_buffer_nested_loop():
    buffer = BetterIterator(["a", "b"])
    result = ""
    for _ in buffer:
        for inner in buffer:
            result += inner
    assert result == "abab"

Summary

Concept map of reflection
Figure 2: Concept map.