How to Use Test Event Handler Registration Using Pytest

I’ve been working on a simple module that registers event handlers registration using pytest and fires them when certain conditions occur. Recently, I wanted to ensure that my event handler registration works correctly by testing that a registered handler is actually called. I’ll walk you through the original code, explain how it works, and then add some extra functionality to practice testing with pytest.

The Original Code

Let’s start with the module that handles event registration:

# mymodule/myfile.py
_event_handlers = []

def register_event_handler(handler):
_event_handlers.append(handler)

def fire_event_handlers():
for handler in _event_handlers:
handler()

Explanation

  • Event Handlers List:
    The module maintains a private list, _event_handlers, where all the event handler functions are stored.
  • Register Function:
    register_event_handler(handler) simply appends the provided handler to the _event_handlers list.
  • Fire Function:
    fire_event_handlers() iterates over each handler in the list and calls it.

This design is straightforward. The main functionality is to allow any part of your application to register a callback (or event handler) that gets executed later when fire_event_handlers() is invoked.

Testing the Registration

Now, here’s a basic test that registers an event handler and fires it:

# tests/test_mymodule.py
from mymodule.myfile import register_event_handler, fire_event_handlers

def test_event_handler():
def example_handler():
pass

register_event_handler(example_handler)
fire_event_handlers()
# assert example_handler was called

The Testing Challenge

The challenge here is: How do I verify that example_handler was called?

Since the handler is a plain function and we are not using any mocks from unittest.mock, we need a different approach to capture the call.

Testing Without Mocks

One simple way to test if a function was called is by using a mutable object—like a list or a dictionary—as a flag. When the handler is called, it can modify this object, and then we can assert that the change occurred.

Here’s how to do it:

# tests/test_mymodule.py
from mymodule.myfile import register_event_handler, fire_event_handlers

def test_event_handler_called():
# Use a list as a mutable flag
call_flag = {"called": False}

def example_handler():
call_flag["called"] = True

register_event_handler(example_handler)
fire_event_handlers()

# Assert that our flag indicates the handler was called
assert call_flag["called"] == True

How It Works

  • Mutable Flag:
    We define call_flag as a dictionary with a key "called" initialized to False. Dictionaries are mutable, so any changes inside the handler persist outside its scope.
  • Handler Modification:
    In example_handler, we set call_flag["called"] to True.
  • Assertion:
    After firing the event handlers, we assert that call_flag["called"] is now True, confirming that example_handler was executed.

Extra Practice Functionality

To further explore event handling, I decided to extend the module with an unregister feature. This way, we can not only register handlers but also remove them when needed.

Extended Module Code

# mymodule/myfile.py
_event_handlers = []

def register_event_handler(handler):
_event_handlers.append(handler)

def unregister_event_handler(handler):
if handler in _event_handlers:
_event_handlers.remove(handler)

def fire_event_handlers():
for handler in _event_handlers:
handler()

def clear_event_handlers():
"""Helper function for testing: clear all registered handlers."""
_event_handlers.clear()

Testing the Extended Functionality

Now, we can add tests to verify that unregistering works:

# tests/test_mymodule_extended.py
from mymodule.myfile import register_event_handler, unregister_event_handler, fire_event_handlers, clear_event_handlers

def test_unregister_event_handler():
call_flag = {"called": False}

def example_handler():
call_flag["called"] = True

# Clear any previous state
clear_event_handlers()

# Register and then unregister the handler
register_event_handler(example_handler)
unregister_event_handler(example_handler)
fire_event_handlers()

# Since the handler was unregistered, it should not have been called.
assert call_flag["called"] == False

def test_multiple_event_handlers():
call_list = []

def handler_one():
call_list.append("one")

def handler_two():
call_list.append("two")

# Clear previous state
clear_event_handlers()

# Register multiple handlers
register_event_handler(handler_one)
register_event_handler(handler_two)
fire_event_handlers()

# Verify both handlers were called in the order of registration
assert call_list == ["one", "two"]

Explanation

  • Unregister Test:
    We register example_handler, then unregister it, and finally fire the events. The flag should remain False since the handler was removed.
  • Multiple Handlers Test:
    This test ensures that when multiple handlers are registered, they are called in the order they were added, and both modify a shared list accordingly.

Final Thoughts

Testing event-driven functionality without heavy reliance on mocking can be both simple and effective by leveraging mutable objects to capture state changes. Adding extra functionality like unregistering handlers not only makes your module more robust but also provides further opportunities to practice writing comprehensive tests with pytest.

Related blog posts