← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:remove-test-event-listener into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:remove-test-event-listener into launchpad:master.

Commit message:
Replace TestEventListener with ZopeEventHandlerFixture

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/374628

After adding optional support to ZopeEventHandlerFixture for specifying
required interfaces, it becomes a superset of the old TestEventListener,
with two clear advantages: it can unregister event handlers on cleanup,
and it uses the fixture pattern which is much more common for this sort
of thing nowadays.  Convert all users of TestEventListener to
ZopeEventHandlerFixture.

(This is very slightly more verbose in doctests because we have to call
the fixture's setUp method separately; but this is minor, and in
exchange self.useFixture makes it less verbose in tests using
testtools.)
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:remove-test-event-listener into launchpad:master.
diff --git a/lib/lp/answers/browser/tests/views.txt b/lib/lp/answers/browser/tests/views.txt
index 975bb8f..b8cf752 100644
--- a/lib/lp/answers/browser/tests/views.txt
+++ b/lib/lp/answers/browser/tests/views.txt
@@ -30,13 +30,14 @@ Register an event listener that will print events it receives.
 
     >>> from lazr.lifecycle.interfaces import IObjectModifiedEvent
     >>> from lp.answers.interfaces.question import IQuestion
-    >>> from lp.testing.event import TestEventListener
+    >>> from lp.testing.fixture import ZopeEventHandlerFixture
 
     >>> def print_modified_event(object, event):
     ...     print("Received ObjectModifiedEvent: %s" % (
     ...         ", ".join(sorted(event.edited_fields))))
-    >>> question_event_listener = TestEventListener(
-    ...     IQuestion, IObjectModifiedEvent, print_modified_event)
+    >>> question_event_listener = ZopeEventHandlerFixture(
+    ...     print_modified_event, (IQuestion, IObjectModifiedEvent))
+    >>> question_event_listener.setUp()
 
     >>> view = create_initialized_view(question_three, name='+subscribe')
     >>> print(view.label)
@@ -82,7 +83,7 @@ Unsubscription works in a similar manner.
     >>> view.request.response.getHeader('Location')
     '.../+question/3'
 
-    >>> question_event_listener.unregister()
+    >>> question_event_listener.cleanUp()
 
 
 QuestionWorkflowView
diff --git a/lib/lp/answers/doc/workflow.txt b/lib/lp/answers/doc/workflow.txt
index bdb5841..3cabebb 100644
--- a/lib/lp/answers/doc/workflow.txt
+++ b/lib/lp/answers/doc/workflow.txt
@@ -604,17 +604,19 @@ the message they create and a ObjectModifiedEvent for the question.
     # Register an event listener that will print events it receives.
     >>> from lazr.lifecycle.interfaces import (
     ...     IObjectCreatedEvent, IObjectModifiedEvent)
-    >>> from lp.testing.event import TestEventListener
+    >>> from lp.testing.fixture import ZopeEventHandlerFixture
     >>> from lp.answers.interfaces.question import IQuestion
 
     >>> def print_event(object, event):
     ...     print("Received %s on %s" % (
     ...         event.__class__.__name__.split('.')[-1],
     ...         object.__class__.__name__.split('.')[-1]))
-    >>> questionmessage_event_listener = TestEventListener(
-    ...     IQuestionMessage, IObjectCreatedEvent, print_event)
-    >>> question_event_listener = TestEventListener(
-    ...     IQuestion, IObjectModifiedEvent, print_event)
+    >>> questionmessage_event_listener = ZopeEventHandlerFixture(
+    ...     print_event, (IQuestionMessage, IObjectCreatedEvent))
+    >>> questionmessage_event_listener.setUp()
+    >>> question_event_listener = ZopeEventHandlerFixture(
+    ...     print_event, (IQuestion, IObjectModifiedEvent))
+    >>> question_event_listener.setUp()
 
 Changing the status triggers the event.
 
@@ -639,8 +641,8 @@ these events.
     Received ObjectModifiedEvent on Question
 
     # Cleanup
-    >>> questionmessage_event_listener.unregister()
-    >>> question_event_listener.unregister()
+    >>> questionmessage_event_listener.cleanUp()
+    >>> question_event_listener.cleanUp()
 
 
 Reopening the question
@@ -652,8 +654,9 @@ is reopened, a QuestionReopening is created.
     # Register an event listener to notify us whenever a QuestionReopening is
     # created.
     >>> from lp.answers.interfaces.questionreopening import IQuestionReopening
-    >>> reopening_event_listener = TestEventListener(
-    ...     IQuestionReopening, IObjectCreatedEvent, print_event)
+    >>> reopening_event_listener = ZopeEventHandlerFixture(
+    ...     print_event, (IQuestionReopening, IObjectCreatedEvent))
+    >>> reopening_event_listener.setUp()
 
 The most common use case is when a user confirms a solution, and then
 comes back to say that it doesn't, in fact, work.
@@ -732,7 +735,7 @@ having been rejected.
     INVALID
 
     # Cleanup
-    >>> reopening_event_listener.unregister()
+    >>> reopening_event_listener.cleanUp()
 
 
 Using an IMessage as an explanation
diff --git a/lib/lp/answers/tests/test_question_workflow.py b/lib/lp/answers/tests/test_question_workflow.py
index d2af2e1..6450d61 100644
--- a/lib/lp/answers/tests/test_question_workflow.py
+++ b/lib/lp/answers/tests/test_question_workflow.py
@@ -20,7 +20,6 @@ from datetime import (
     timedelta,
     )
 import traceback
-import unittest
 
 from lazr.lifecycle.interfaces import (
     IObjectCreatedEvent,
@@ -54,12 +53,13 @@ from lp.testing import (
     ANONYMOUS,
     login,
     login_person,
+    TestCase,
     )
-from lp.testing.event import TestEventListener
+from lp.testing.fixture import ZopeEventHandlerFixture
 from lp.testing.layers import DatabaseFunctionalLayer
 
 
-class BaseAnswerTrackerWorkflowTestCase(unittest.TestCase):
+class BaseAnswerTrackerWorkflowTestCase(TestCase):
     """Base class for test cases related to the Answer Tracker workflow.
 
     It provides the common fixture and test helper methods.
@@ -68,6 +68,8 @@ class BaseAnswerTrackerWorkflowTestCase(unittest.TestCase):
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
+        super(BaseAnswerTrackerWorkflowTestCase, self).setUp()
+
         self.now = datetime.now(UTC)
 
         # Login as the question owner.
@@ -90,10 +92,7 @@ class BaseAnswerTrackerWorkflowTestCase(unittest.TestCase):
             self.owner, 'Help!', 'I need help with Ubuntu',
             datecreated=self.now)
 
-    def tearDown(self):
-        if hasattr(self, 'created_event_listener'):
-            self.created_event_listener.unregister()
-            self.modified_event_listener.unregister()
+        self.registered_event_listeners = False
 
     def setQuestionStatus(self, question, new_status,
                           comment="Status change."):
@@ -110,13 +109,12 @@ class BaseAnswerTrackerWorkflowTestCase(unittest.TestCase):
     def setUpEventListeners(self):
         """Install a listener for events emitted during the test."""
         self.collected_events = []
-        if hasattr(self, 'modified_event_listener'):
-            # Event listeners is already registered.
-            return
-        self.modified_event_listener = TestEventListener(
-            IQuestion, IObjectModifiedEvent, self.collectEvent)
-        self.created_event_listener = TestEventListener(
-            IQuestionMessage, IObjectCreatedEvent, self.collectEvent)
+        if not self.registered_event_listeners:
+            self.useFixture(ZopeEventHandlerFixture(
+                self.collectEvent, (IQuestion, IObjectModifiedEvent)))
+            self.useFixture(ZopeEventHandlerFixture(
+                self.collectEvent, (IQuestionMessage, IObjectCreatedEvent)))
+            self.registered_event_listeners = True
 
     def collectEvent(self, object, event):
         """Collect events"""
@@ -518,7 +516,7 @@ class LinkFAQTestCase(BaseAnswerTrackerWorkflowTestCase):
 
     def setUp(self):
         """Create an additional FAQ."""
-        BaseAnswerTrackerWorkflowTestCase.setUp(self)
+        super(LinkFAQTestCase, self).setUp()
 
         # Only admin can create FAQ on ubuntu.
         login_person(self.admin)
diff --git a/lib/lp/bugs/browser/tests/bug-views.txt b/lib/lp/bugs/browser/tests/bug-views.txt
index fb41886..2e89e54 100644
--- a/lib/lp/bugs/browser/tests/bug-views.txt
+++ b/lib/lp/bugs/browser/tests/bug-views.txt
@@ -8,16 +8,18 @@ There are three objects on which you can file a bug. An
 ObjectCreatedEvent is published when the bug is filed. Let's register
 an event listener to demonstrate this.
 
-    >>> from lp.services.database.sqlbase import flush_database_updates
+    >>> from lazr.lifecycle.event import IObjectCreatedEvent
     >>> import transaction
 
-    >>> from lp.testing.event import TestEventListener
-    >>> from lazr.lifecycle.event import IObjectCreatedEvent
     >>> from lp.bugs.interfaces.bug import IBug
+    >>> from lp.services.database.sqlbase import flush_database_updates
+    >>> from lp.testing.fixture import ZopeEventHandlerFixture
+
     >>> def on_created_event(object, event):
     ...     print("ObjectCreatedEvent: %r" % object)
-    >>> on_created_listener = TestEventListener(
-    ...     IBug, IObjectCreatedEvent, on_created_event)
+    >>> on_created_listener = ZopeEventHandlerFixture(
+    ...     on_created_event, (IBug, IObjectCreatedEvent))
+    >>> on_created_listener.setUp()
 
 1. Filing a bug on a distribution.
 
@@ -210,7 +212,7 @@ indentical to the second, we really only display one comment:
 
 (Unregister our listener, since we no longer need it.)
 
-    >>> on_created_listener.unregister()
+    >>> on_created_listener.cleanUp()
 
 
 Bug Portlets
diff --git a/lib/lp/bugs/browser/tests/buglinktarget-views.txt b/lib/lp/bugs/browser/tests/buglinktarget-views.txt
index 99fd6a2..86e5baf 100644
--- a/lib/lp/bugs/browser/tests/buglinktarget-views.txt
+++ b/lib/lp/bugs/browser/tests/buglinktarget-views.txt
@@ -13,11 +13,12 @@ The +linkbug and +unlinkbug views operates on IBugLinkTarget.
     >>> cve = getUtility(ICveSet)['2005-2730']
 
     (Setup an event listener.)
-    >>> from lp.testing.event import TestEventListener
+    >>> from lp.testing.fixture import ZopeEventHandlerFixture
     >>> collected_events = []
-    >>> modified_listener = TestEventListener(
-    ...     IBugLinkTarget, ObjectModifiedEvent,
-    ...     lambda object, event: collected_events.append(event))
+    >>> modified_listener = ZopeEventHandlerFixture(
+    ...     lambda object, event: collected_events.append(event),
+    ...     (IBugLinkTarget, ObjectModifiedEvent))
+    >>> modified_listener.setUp()
 
     (Login because bug link management is only available to registered users.)
     >>> login('no-priv@xxxxxxxxxxxxx')
@@ -151,4 +152,4 @@ The notification contains the escaped bug title.
 == Cleanup ==
 
     (Deactivate the event listener.)
-    >>> modified_listener.unregister()
+    >>> modified_listener.cleanUp()
diff --git a/lib/lp/bugs/browser/tests/bugtask-adding-views.txt b/lib/lp/bugs/browser/tests/bugtask-adding-views.txt
index f1d3c87..b0200e2 100644
--- a/lib/lp/bugs/browser/tests/bugtask-adding-views.txt
+++ b/lib/lp/bugs/browser/tests/bugtask-adding-views.txt
@@ -171,14 +171,15 @@ In order to show that all the events get fired off, let's create an
 event listener and register it:
 
     >>> from zope.interface import Interface
-    >>> from lp.testing.event import TestEventListener
     >>> from lazr.lifecycle.interfaces import IObjectCreatedEvent
+    >>> from lp.testing.fixture import ZopeEventHandlerFixture
 
     >>> def on_created_event(object, event):
     ...     print("ObjectCreatedEvent: %r" % object)
 
-    >>> on_created_listener = TestEventListener(
-    ...     Interface, IObjectCreatedEvent, on_created_event)
+    >>> on_created_listener = ZopeEventHandlerFixture(
+    ...     on_created_event, (Interface, IObjectCreatedEvent))
+    >>> on_created_listener.setUp()
 
 
 If an invalid product is specified, or a product that fails the
@@ -449,7 +450,7 @@ package will be converted to the corresponding source package.
     ...
     mozilla-firefox (Ubuntu)
 
-    >>> on_created_listener.unregister()
+    >>> on_created_listener.cleanUp()
 
 
 Registering a product while adding a bugtask
diff --git a/lib/lp/bugs/doc/bugattachments.txt b/lib/lp/bugs/doc/bugattachments.txt
index 4788804..bbd23bd 100644
--- a/lib/lp/bugs/doc/bugattachments.txt
+++ b/lib/lp/bugs/doc/bugattachments.txt
@@ -28,12 +28,13 @@ ObjectCreatedEvent in order to trigger email notifications:
 
     >>> from io import BytesIO
 
-    >>> from lp.testing.event import TestEventListener
     >>> from lazr.lifecycle.event import IObjectCreatedEvent
+    >>> from lp.testing.fixture import ZopeEventHandlerFixture
     >>> def attachment_added(attachment, event):
     ...     print("Attachment added: %r" % attachment.libraryfile.filename)
-    >>> event_listener = TestEventListener(
-    ...     IBugAttachment, IObjectCreatedEvent, attachment_added)
+    >>> event_listener = ZopeEventHandlerFixture(
+    ...     attachment_added, (IBugAttachment, IObjectCreatedEvent))
+    >>> event_listener.setUp()
 
     >>> filecontent = b"Some useful information."
     >>> data = BytesIO(filecontent)
@@ -293,7 +294,7 @@ from the librarian.
     'http://.../foo-bar-baz'
 
     >>> config_data = config.pop('max_attachment_size')
-    >>> event_listener.unregister()
+    >>> event_listener.cleanUp()
 
 
 Security
diff --git a/lib/lp/bugs/doc/bugwatch.txt b/lib/lp/bugs/doc/bugwatch.txt
index 7dba686..c280375 100644
--- a/lib/lp/bugs/doc/bugwatch.txt
+++ b/lib/lp/bugs/doc/bugwatch.txt
@@ -263,11 +263,12 @@ we can confirm that an event is indeed fired off.
     ...     if bugtask.importance != old_bugtask.importance:
     ...         print("%s => %s" % (old_bugtask.importance.title,
     ...             bugtask.importance.title))
-    >>> from lp.testing.event import TestEventListener
     >>> from lazr.lifecycle.interfaces import IObjectModifiedEvent
     >>> from lp.bugs.interfaces.bugtask import IBugTask
-    >>> event_listener = TestEventListener(
-    ...     IBugTask, IObjectModifiedEvent, print_bugtask_modified)
+    >>> from lp.testing.fixture import ZopeEventHandlerFixture
+    >>> event_listener = ZopeEventHandlerFixture(
+    ...     print_bugtask_modified, (IBugTask, IObjectModifiedEvent))
+    >>> event_listener.setUp()
 
 If we pass in a different Malone status than the existing one, an event
 will be fired off, even though the remote status stays the same.
@@ -391,7 +392,7 @@ manner:
     ** Changed in: mozilla-firefox (Debian)
        Importance: Low => Critical
 
-    >>> event_listener.unregister()
+    >>> event_listener.cleanUp()
 
 The Bug Watch Updater can transition a bug to any status or importance:
 
diff --git a/lib/lp/bugs/doc/malone-xmlrpc.txt b/lib/lp/bugs/doc/malone-xmlrpc.txt
index 78a0ddb..1d4ace3 100644
--- a/lib/lp/bugs/doc/malone-xmlrpc.txt
+++ b/lib/lp/bugs/doc/malone-xmlrpc.txt
@@ -49,15 +49,16 @@ First, let's define a simple event listener to show that the
 IObjectCreatedEvent is being published when a bug is reported through
 the XML-RPC interface.
 
-    >>> from lp.testing.event import TestEventListener
     >>> from lazr.lifecycle.interfaces import IObjectCreatedEvent
     >>> from lp.bugs.interfaces.bug import IBug
+    >>> from lp.testing.fixture import ZopeEventHandlerFixture
 
     >>> def on_created_event(obj, event):
     ...     print("ObjectCreatedEvent: %r" % obj)
 
-    >>> on_created_listener = TestEventListener(
-    ...     IBug, IObjectCreatedEvent, on_created_event)
+    >>> on_created_listener = ZopeEventHandlerFixture(
+    ...     on_created_event, (IBug, IObjectCreatedEvent))
+    >>> on_created_listener.setUp()
 
 Reporting a product bug.
 
@@ -228,7 +229,7 @@ Invalid subscriber.
     Fault: <Fault 20: 'Invalid subscriber: No user with the email address
             "nosuch@xxxxxxxxxxxxxx" was found'>
 
-    >>> on_created_listener.unregister()
+    >>> on_created_listener.cleanUp()
 
 
 Generating bugtracker authentication tokens
diff --git a/lib/lp/bugs/tests/buglinktarget.txt b/lib/lp/bugs/tests/buglinktarget.txt
index 733d8c1..556b67c 100644
--- a/lib/lp/bugs/tests/buglinktarget.txt
+++ b/lib/lp/bugs/tests/buglinktarget.txt
@@ -46,11 +46,12 @@ fired.
     >>> from zope.interface import Interface
     >>> from lp.bugs.interfaces.buglink import (
     ...     IObjectLinkedEvent, IObjectUnlinkedEvent)
-    >>> from lp.testing.event import TestEventListener
+    >>> from lp.testing.fixture import ZopeEventHandlerFixture
     >>> linked_events = []
-    >>> linked_event_listener = TestEventListener(
-    ...     Interface, IObjectLinkedEvent,
-    ...     lambda object, event: linked_events.append(event))
+    >>> linked_event_listener = ZopeEventHandlerFixture(
+    ...     lambda object, event: linked_events.append(event),
+    ...     (Interface, IObjectLinkedEvent))
+    >>> linked_event_listener.setUp()
 
     >>> bug2 = bugset.get(2)
     >>> target.linkBug(bugset.get(2))
@@ -125,9 +126,10 @@ The method returns whether the link existed. It should also send an
 IObjectUnlinkedEvent for each of the removed link:
 
     >>> unlinked_events = []
-    >>> unlinked_event_listener = TestEventListener(
-    ...     Interface, IObjectUnlinkedEvent,
-    ...     lambda object, event: unlinked_events.append(event))
+    >>> unlinked_event_listener = ZopeEventHandlerFixture(
+    ...     lambda object, event: unlinked_events.append(event),
+    ...     (Interface, IObjectUnlinkedEvent))
+    >>> unlinked_event_listener.setUp()
 
     >>> target.unlinkBug(bug1, factory.makePerson())
     True
@@ -167,5 +169,5 @@ the bug or if they are an administrator.
 == Cleanup ==
 
     # Unregister event listeners.
-    >>> linked_event_listener.unregister()
-    >>> unlinked_event_listener.unregister()
+    >>> linked_event_listener.cleanUp()
+    >>> unlinked_event_listener.cleanUp()
diff --git a/lib/lp/bugs/tests/bugs-emailinterface.txt b/lib/lp/bugs/tests/bugs-emailinterface.txt
index ab8fd0d..2bc5460 100644
--- a/lib/lp/bugs/tests/bugs-emailinterface.txt
+++ b/lib/lp/bugs/tests/bugs-emailinterface.txt
@@ -1600,12 +1600,14 @@ An email can contain multiple commands, even for different bugs.
     ...         bugtask.importance.title))
     >>> from lazr.lifecycle.interfaces import (
     ...     IObjectCreatedEvent, IObjectModifiedEvent)
-    >>> from lp.testing.event import TestEventListener
     >>> from lp.bugs.interfaces.bugtask import IBugTask
-    >>> bugtask_modified_listener = TestEventListener(
-    ...     IBugTask, IObjectModifiedEvent, print_bugtask_modified_event)
-    >>> bugtask_created_listener = TestEventListener(
-    ...     IBugTask, IObjectCreatedEvent, print_bugtask_created_event)
+    >>> from lp.testing.fixture import ZopeEventHandlerFixture
+    >>> bugtask_modified_listener = ZopeEventHandlerFixture(
+    ...     print_bugtask_modified_event, (IBugTask, IObjectModifiedEvent))
+    >>> bugtask_modified_listener.setUp()
+    >>> bugtask_created_listener = ZopeEventHandlerFixture(
+    ...     print_bugtask_created_event, (IBugTask, IObjectCreatedEvent))
+    >>> bugtask_created_listener.setUp()
     >>> bug_four_upstream_task = bug_four.bugtasks[0]
     >>> print(bug_four_upstream_task.status.name)
     NEW
@@ -1634,8 +1636,8 @@ An email can contain multiple commands, even for different bugs.
     >>> print(bug_five_upstream_task.importance.name)
     HIGH
 
-    >>> bugtask_modified_listener.unregister()
-    >>> bugtask_created_listener.unregister()
+    >>> bugtask_modified_listener.cleanUp()
+    >>> bugtask_created_listener.cleanUp()
 
 
 Default 'affects' target
diff --git a/lib/lp/code/model/tests/test_codereviewkarma.py b/lib/lp/code/model/tests/test_codereviewkarma.py
index bd6a4de..d763ba5 100644
--- a/lib/lp/code/model/tests/test_codereviewkarma.py
+++ b/lib/lp/code/model/tests/test_codereviewkarma.py
@@ -14,7 +14,7 @@ from lp.testing import (
     login_person,
     TestCaseWithFactory,
     )
-from lp.testing.event import TestEventListener
+from lp.testing.fixture import ZopeEventHandlerFixture
 from lp.testing.layers import DatabaseFunctionalLayer
 
 
@@ -27,30 +27,15 @@ class TestCodeReviewKarma(TestCaseWithFactory):
     """
 
     layer = DatabaseFunctionalLayer
-    karma_listener = None
 
     def setUp(self):
         # Use an admin to get launchpad.Edit on all the branches to easily
         # approve and reject the proposals.
         super(TestCodeReviewKarma, self).setUp('admin@xxxxxxxxxxxxx')
-        # The way the zope infrastructure works is that we can register
-        # subscribers easily, but there is no way to unregister them (bug
-        # 2338).  TestEventListener does this with by setting a property to
-        # stop calling the callback function.  Instead of ending up with a
-        # whole pile of registered inactive event listeners, we just
-        # reactivate the one we have if there is one.
-        if self.karma_listener is None:
-            self.karma_listener = TestEventListener(
-                IPerson, IKarmaAssignedEvent, self._on_karma_assigned)
-        else:
-            self.karma_listener._active = True
-
+        self.useFixture(ZopeEventHandlerFixture(
+            self._on_karma_assigned, (IPerson, IKarmaAssignedEvent)))
         self.karma_events = []
 
-    def tearDown(self):
-        self.karma_listener.unregister()
-        super(TestCodeReviewKarma, self).tearDown()
-
     def _on_karma_assigned(self, object, event):
         # Store the karma event for checking in the test method.
         self.karma_events.append(event.karma)
diff --git a/lib/lp/registry/doc/milestone.txt b/lib/lp/registry/doc/milestone.txt
index 451bb8e..2ad094c 100644
--- a/lib/lp/registry/doc/milestone.txt
+++ b/lib/lp/registry/doc/milestone.txt
@@ -510,9 +510,9 @@ When a milestone with bug tasks creates a release, those bug tasks in
 fix committed status are updated to fix released. An ObjectModifiedEvent
 event is signaled for each changed bug task.
 
-    >>> from lp.testing.event import TestEventListener
     >>> from lazr.lifecycle.interfaces import IObjectModifiedEvent
     >>> from lp.bugs.interfaces.bugtask import BugTaskStatus, IBugTask
+    >>> from lp.testing.fixture import ZopeEventHandlerFixture
 
     >>> def print_event(object, event):
     ...     print "Received %s on %s" % (
@@ -527,8 +527,9 @@ event is signaled for each changed bug task.
     >>> triaged_bugtask.transitionToMilestone(milestone, owner)
     >>> triaged_bugtask.transitionToStatus(BugTaskStatus.TRIAGED, owner)
     >>> release = milestone.createProductRelease(owner, datetime.now(UTC))
-    >>> bugtask_event_listener = TestEventListener(
-    ...     IBugTask, IObjectModifiedEvent, print_event)
+    >>> bugtask_event_listener = ZopeEventHandlerFixture(
+    ...     print_event, (IBugTask, IObjectModifiedEvent))
+    >>> bugtask_event_listener.setUp()
 
     >>> milestone.closeBugsAndBlueprints(owner)
     Received ObjectModifiedEvent on BugTask
@@ -539,6 +540,6 @@ event is signaled for each changed bug task.
     >>> triaged_bugtask.status
     <DBItem BugTaskStatus.TRIAGED, (21) Triaged>
 
-    >>> bugtask_event_listener.unregister()
+    >>> bugtask_event_listener.cleanUp()
 
 
diff --git a/lib/lp/registry/doc/person.txt b/lib/lp/registry/doc/person.txt
index 9a49a80..11abc90 100644
--- a/lib/lp/registry/doc/person.txt
+++ b/lib/lp/registry/doc/person.txt
@@ -442,16 +442,17 @@ PersonSet.newTeam() will also fire an ObjectCreatedEvent for the newly
 created team.
 
     >>> from zope.lifecycleevent.interfaces import IObjectCreatedEvent
-    >>> from lp.testing.event import TestEventListener
+    >>> from lp.testing.fixture import ZopeEventHandlerFixture
     >>> def print_event(team, event):
     ...     print "ObjectCreatedEvent fired for team '%s'" % team.name
 
-    >>> listener = TestEventListener(
-    ...     ITeam, IObjectCreatedEvent, print_event)
+    >>> listener = ZopeEventHandlerFixture(
+    ...     print_event, (ITeam, IObjectCreatedEvent))
+    >>> listener.setUp()
     >>> another_team = personset.newTeam(ddaa, 'new3', 'Another a new team')
     ObjectCreatedEvent fired for team 'new3'
 
-    >>> listener.unregister()
+    >>> listener.cleanUp()
 
 
 Turning people into teams
diff --git a/lib/lp/registry/tests/test_product.py b/lib/lp/registry/tests/test_product.py
index 6dc0e96..00e840d 100644
--- a/lib/lp/registry/tests/test_product.py
+++ b/lib/lp/registry/tests/test_product.py
@@ -103,7 +103,7 @@ from lp.testing import (
     TestCaseWithFactory,
     WebServiceTestCase,
     )
-from lp.testing.event import TestEventListener
+from lp.testing.fixture import ZopeEventHandlerFixture
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
@@ -1514,16 +1514,11 @@ class ProductLicensingTestCase(TestCaseWithFactory):
     """Test the rules of licences and commercial subscriptions."""
 
     layer = DatabaseFunctionalLayer
-    event_listener = None
 
     def setup_event_listener(self):
         self.events = []
-        if self.event_listener is None:
-            self.event_listener = TestEventListener(
-                IProduct, IObjectModifiedEvent, self.on_event)
-        else:
-            self.event_listener._active = True
-        self.addCleanup(self.event_listener.unregister)
+        self.useFixture(ZopeEventHandlerFixture(
+            self.on_event, (IProduct, IObjectModifiedEvent)))
 
     def on_event(self, thing, event):
         self.events.append(event)
diff --git a/lib/lp/testing/event.py b/lib/lp/testing/event.py
deleted file mode 100644
index 4bf85ab..0000000
--- a/lib/lp/testing/event.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Helper class for checking the event notifications."""
-
-__metaclass__ = type
-
-from zope.app.testing import ztapi
-
-
-class TestEventListener:
-    """Listen for a specific object event in tests.
-
-    When an event of the specified type is fired off for an object with
-    the specifed type, the given callback is called.
-
-    The callback function should take an object and an event.
-
-    At the end of the test you have to unregister the event listener
-    using event_listener.unregister().
-    """
-
-    def __init__(self, object_type, event_type, callback):
-        self.object_type = object_type
-        self.event_type = event_type
-        self.callback = callback
-        self._active = True
-        ztapi.subscribe((object_type, event_type), None, self)
-
-    def __call__(self, object, event):
-        if not self._active:
-            return
-        self.callback(object, event)
-
-    def unregister(self):
-        """Stop the event listener from listening to events."""
-        # XXX: Bjorn Tillenius 2006-02-14 bug=2338: There is currently no way
-        #      of unsubscribing an event handler, so we simply set
-        #      self._active to False in order to make the handler return
-        #      without doing anything.
-        self._active = False
diff --git a/lib/lp/testing/fixture.py b/lib/lp/testing/fixture.py
index 9a530fe..557a385 100644
--- a/lib/lp/testing/fixture.py
+++ b/lib/lp/testing/fixture.py
@@ -42,7 +42,6 @@ from wsgi_intercept.urllib2_intercept import (
 from zope.component import (
     adapter,
     getGlobalSiteManager,
-    provideHandler,
     )
 from zope.interface import Interface
 from zope.publisher.interfaces.browser import IDefaultBrowserLayer
@@ -169,14 +168,16 @@ class ZopeAdapterFixture(Fixture):
 class ZopeEventHandlerFixture(Fixture):
     """A fixture that provides and then unprovides a Zope event handler."""
 
-    def __init__(self, handler):
+    def __init__(self, handler, required=None):
         super(ZopeEventHandlerFixture, self).__init__()
         self._handler = handler
+        self._required = required
 
     def _setUp(self):
         gsm = getGlobalSiteManager()
-        provideHandler(self._handler)
-        self.addCleanup(gsm.unregisterHandler, self._handler)
+        gsm.registerHandler(self._handler, required=self._required)
+        self.addCleanup(
+            gsm.unregisterHandler, self._handler, required=self._required)
 
 
 class ZopeViewReplacementFixture(Fixture):
diff --git a/lib/lp/testing/karma.py b/lib/lp/testing/karma.py
index c0eaf8a..801b9ff 100644
--- a/lib/lp/testing/karma.py
+++ b/lib/lp/testing/karma.py
@@ -13,9 +13,10 @@ __all__ = [
 
 from lp.registry.interfaces.karma import IKarmaAssignedEvent
 from lp.registry.interfaces.person import IPerson
-from lp.testing.event import TestEventListener
+from lp.testing.fixture import ZopeEventHandlerFixture
 
 
+# XXX cjwatson 2019-10-23: This should be a fixture.
 class KarmaRecorder:
     """Helper that records selected karma events.
 
@@ -80,12 +81,14 @@ class KarmaRecorder:
 
     def register_listener(self):
         """Register listener.  Must be `unregister`ed later."""
-        self.listener = TestEventListener(
-            IPerson, IKarmaAssignedEvent, self.receive)
+        self.listener = ZopeEventHandlerFixture(
+            self.receive, (IPerson, IKarmaAssignedEvent))
+        self.listener.setUp()
 
     def unregister_listener(self):
         """Unregister listener after `register`."""
-        self.listener.unregister()
+        self.listener.cleanUp()
+        self.listener = None
 
 
 class KarmaAssignedEventListener(KarmaRecorder):