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 definecall_flag
as a dictionary with a key"called"
initialized toFalse
. Dictionaries are mutable, so any changes inside the handler persist outside its scope. - Handler Modification:
Inexample_handler
, we setcall_flag["called"]
toTrue
. - Assertion:
After firing the event handlers, we assert thatcall_flag["called"]
is nowTrue
, confirming thatexample_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 registerexample_handler
, then unregister it, and finally fire the events. The flag should remainFalse
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.