diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py
index 98eb8da61c3718281cbdf020637d0b6c151ff09f..1b8874e4a7a8353c7112906b2459a85f710efa60 100644
--- a/src/_zkapauthorizer/_plugin.py
+++ b/src/_zkapauthorizer/_plugin.py
@@ -185,7 +185,7 @@ class ZKAPAuthorizer(object):
         )
 
 
-    def get_client_resource(self, node_config, default_token_count=None):
+    def get_client_resource(self, node_config, default_token_count=None, reactor=None):
         """
         Get an ``IZKAPRoot`` for the given node configuration.
 
@@ -197,12 +197,14 @@ class ZKAPAuthorizer(object):
             This is only used if a number of tokens isn't specified at the
             point of redemption.
         """
-        from twisted.internet import reactor
+        if reactor is None:
+            from twisted.internet import reactor
         return resource_from_configuration(
             node_config,
             store=self._get_store(node_config),
             redeemer=self._get_redeemer(node_config, None, reactor),
             default_token_count=default_token_count,
+            clock=reactor,
         )
 
 
diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index 96d70747cc45f31cebd2c67cc3bcd821c3977e54..15d8395080c10777a57cd227fa05e70ffdc301a5 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -712,6 +712,9 @@ class PaymentController(object):
         TODO: Retrieve this value from the PaymentServer or from the
         ZKAPAuthorizer configuration instead of just hard-coding a duplicate
         value in this implementation.
+
+    :ivar IReactorTime _clock: The reactor to use for scheduling redemption
+        retries.
     """
     _log = Logger()
 
@@ -721,9 +724,7 @@ class PaymentController(object):
 
     num_redemption_groups = attr.ib(default=16)
 
-    _clock = attr.ib(
-        default=attr.Factory(partial(namedAny, "twisted.internet.reactor")),
-    )
+    _clock = attr.ib(default=None)
 
     _error = attr.ib(default=attr.Factory(dict))
     _unpaid = attr.ib(default=attr.Factory(dict))
@@ -735,6 +736,9 @@ class PaymentController(object):
 
         This is an initialization-time hook called by attrs.
         """
+        if self._clock is None:
+            self._clock = namedAny("twisted.internet.reactor")
+
         self._check_pending_vouchers()
         # Also start a time-based polling loop to retry redemption of vouchers
         # in retryable error states.
diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index 96dc29dbaeeda4ed9ee2f04150d5651c85380af4..f8fa7a9dab92f86bba84ece5a21a0e1cfeb603e4 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -91,7 +91,13 @@ class IZKAPRoot(IResource):
     controller = Attribute("The ``PaymentController`` used by this resource tree.")
 
 
-def from_configuration(node_config, store, redeemer=None, default_token_count=None):
+def from_configuration(
+        node_config,
+        store,
+        redeemer=None,
+        default_token_count=None,
+        clock=None,
+):
     """
     Instantiate the plugin root resource using data from its configuration
     section, **storageclient.plugins.privatestorageio-zkapauthz-v1**, in the
@@ -108,6 +114,10 @@ def from_configuration(node_config, store, redeemer=None, default_token_count=No
     :param IRedeemer redeemer: The voucher redeemer to use.  If ``None`` a
         sensible one is constructed.
 
+    :param default_token_count: See ``PaymentController.default_token_count``.
+
+    :param clock: See ``PaymentController._clock``.
+
     :return IZKAPRoot: The root of the resource hierarchy presented by the
         client side of the plugin.
     """
@@ -120,7 +130,12 @@ def from_configuration(node_config, store, redeemer=None, default_token_count=No
         )
     if default_token_count is None:
         default_token_count = NUM_TOKENS
-    controller = PaymentController(store, redeemer, default_token_count)
+    controller = PaymentController(
+        store,
+        redeemer,
+        default_token_count,
+        clock=clock,
+    )
 
     calculator = PriceCalculator(
         get_configured_shares_needed(node_config),
diff --git a/src/_zkapauthorizer/tests/fixtures.py b/src/_zkapauthorizer/tests/fixtures.py
index 00be5b25283194c4a9454d6fa5314a6695b60650..35aadaea07c79020433806bdcf6ccea6cb2e4410 100644
--- a/src/_zkapauthorizer/tests/fixtures.py
+++ b/src/_zkapauthorizer/tests/fixtures.py
@@ -30,7 +30,9 @@ from fixtures import (
 from twisted.python.filepath import (
     FilePath,
 )
-
+from twisted.internet.task import (
+    Clock,
+)
 from allmydata.storage.server import (
     StorageServer,
 )
@@ -125,6 +127,7 @@ class ConfiglessMemoryVoucherStore(Fixture):
             # minimum token count requirement (can't have fewer tokens
             # than groups).
             num_redemption_groups=1,
+            clock=Clock(),
         ).redeem(
             voucher,
         )
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index e13d4747e0c2c3246c4ce012b7da911df0970212..6b38da748c5e7ce8f541b72698f79c974cfab177 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -103,6 +103,7 @@ from twisted.internet.defer import (
 )
 from twisted.internet.task import (
     Cooperator,
+    Clock,
 )
 from twisted.web.http import (
     OK,
@@ -278,6 +279,7 @@ def root_from_config(config, now):
             memory_connect,
         ),
         default_token_count=NUM_TOKENS,
+        clock=Clock(),
     )
 
 
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index bec922bb0863dea1caee2df78cd783e713894d11..25568d09803c21def1bf10a49d7b088e6bf00895 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -73,6 +73,9 @@ from twisted.python.url import (
 from twisted.internet.defer import (
     fail,
 )
+from twisted.internet.task import (
+    Clock,
+)
 from twisted.web.iweb import (
     IAgent,
 )
@@ -228,6 +231,7 @@ class PaymentControllerTests(TestCase):
             store,
             DummyRedeemer(),
             default_token_count=100,
+            clock=Clock(),
         )
 
         self.assertThat(
@@ -263,6 +267,7 @@ class PaymentControllerTests(TestCase):
             store,
             NonRedeemer(),
             default_token_count=100,
+            clock=Clock(),
         )
         self.assertThat(
             controller.redeem(voucher),
@@ -299,6 +304,7 @@ class PaymentControllerTests(TestCase):
             # Require more success than we're going to get so it doesn't
             # finish.
             num_redemption_groups=counter,
+            clock=Clock(),
         )
 
         self.assertThat(
@@ -353,6 +359,7 @@ class PaymentControllerTests(TestCase):
                 ),
                 default_token_count=num_tokens,
                 num_redemption_groups=num_redemption_groups,
+                clock=Clock(),
             )
             self.assertThat(
                 controller.redeem(voucher),
@@ -378,6 +385,7 @@ class PaymentControllerTests(TestCase):
                 # The number of redemption groups must not change for
                 # redemption of a particular voucher.
                 num_redemption_groups=num_redemption_groups,
+                clock=Clock(),
             )
 
         first_try()
@@ -412,6 +420,7 @@ class PaymentControllerTests(TestCase):
             redeemer,
             default_token_count=num_tokens,
             num_redemption_groups=num_redemption_groups,
+            clock=Clock(),
         )
         self.assertThat(
             controller.redeem(voucher),
@@ -435,6 +444,7 @@ class PaymentControllerTests(TestCase):
             store,
             DummyRedeemer(public_key),
             default_token_count=100,
+            clock=Clock(),
         )
         self.assertThat(
             controller.redeem(voucher),
@@ -462,6 +472,7 @@ class PaymentControllerTests(TestCase):
             store,
             DoubleSpendRedeemer(),
             default_token_count=100,
+            clock=Clock(),
         )
         self.assertThat(
             controller.redeem(voucher),
@@ -491,6 +502,7 @@ class PaymentControllerTests(TestCase):
             store,
             UnpaidRedeemer(),
             default_token_count=100,
+            clock=Clock(),
         )
         self.assertThat(
             unpaid_controller.redeem(voucher),
@@ -510,6 +522,7 @@ class PaymentControllerTests(TestCase):
             store,
             DummyRedeemer(),
             default_token_count=100,
+            clock=Clock(),
         )
 
         self.assertThat(
diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py
index 44c79af85c0a783ec4af01ae06cc44309686a000..e28f06be5ec06360346d5790c06df3293479425a 100644
--- a/src/_zkapauthorizer/tests/test_plugin.py
+++ b/src/_zkapauthorizer/tests/test_plugin.py
@@ -99,6 +99,9 @@ from twisted.plugin import (
 from twisted.test.proto_helpers import (
     StringTransport,
 )
+from twisted.internet.task import (
+    Clock,
+)
 from twisted.web.resource import (
     IResource,
 )
@@ -483,6 +486,7 @@ class ClientPluginTests(TestCase):
             DummyRedeemer(),
             default_token_count=num_passes,
             num_redemption_groups=1,
+            clock=Clock(),
         )
         # Get a token inserted into the store.
         redeeming = controller.redeem(voucher)
@@ -543,7 +547,11 @@ class ClientResourceTests(TestCase):
         nodedir = tempdir.join(b"node")
         config = get_config(nodedir, b"tub.port")
         self.assertThat(
-            storage_server.get_client_resource(config, default_token_count=10),
+            storage_server.get_client_resource(
+                config,
+                default_token_count=10,
+                reactor=Clock(),
+            ),
             Provides([IResource]),
         )