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/foolscap.py b/src/_zkapauthorizer/foolscap.py
index 3d69a5358f258f6fa2807e04dc0b0d8563a243ca..29ed94e7374c22b4a7534a9cc843a2db9970be79 100644
--- a/src/_zkapauthorizer/foolscap.py
+++ b/src/_zkapauthorizer/foolscap.py
@@ -31,6 +31,7 @@ from foolscap.api import (
     DictOf,
     ListOf,
     Copyable,
+    RemoteCopy,
 )
 from foolscap.remoteinterface import (
     RemoteMethodSchema,
@@ -44,7 +45,7 @@ from allmydata.interfaces import (
 )
 
 @attr.s
-class ShareStat(Copyable):
+class ShareStat(Copyable, RemoteCopy):
     """
     Represent some metadata about a share.
 
@@ -53,8 +54,16 @@ class ShareStat(Copyable):
     :ivar int lease_expiration: The POSIX timestamp of the time at which the
         lease on this share expires, or None if there is no lease.
     """
-    size = attr.ib()
-    lease_expiration = attr.ib()
+    typeToCopy = copytype = "ShareStat"
+
+    # To be a RemoteCopy it must be possible to instantiate this with no
+    # arguments. :/ So supply defaults for these attributes.
+    size = attr.ib(default=0)
+    lease_expiration = attr.ib(default=0)
+
+    # The RemoteCopy interface
+    def setCopyableState(self, state):
+        self.__dict__ = state
 
 
 # The Foolscap convention seems to be to try to constrain inputs to valid
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/foolscap.py b/src/_zkapauthorizer/tests/foolscap.py
index c9bb485a927b8884b131c3b3e4f6c36fe264aadf..35716998d69bb32c5066f244e285aececd0bf6ba 100644
--- a/src/_zkapauthorizer/tests/foolscap.py
+++ b/src/_zkapauthorizer/tests/foolscap.py
@@ -27,11 +27,19 @@ from zope.interface import (
 import attr
 
 from twisted.internet.defer import (
-    execute,
+    succeed,
+    fail,
 )
 
 from foolscap.api import (
     RemoteInterface,
+    Referenceable,
+    Copyable,
+    Any,
+)
+from foolscap.copyable import (
+    ICopyable,
+    CopyableSlicer,
 )
 
 from allmydata.interfaces import (
@@ -41,6 +49,11 @@ from allmydata.interfaces import (
 class RIStub(RemoteInterface):
     pass
 
+
+class RIEcho(RemoteInterface):
+    def echo(argument=Any()):
+        return Any()
+
 @implementer(RIStorageServer)
 class StubStorageServer(object):
     pass
@@ -50,6 +63,18 @@ def get_anonymous_storage_server():
     return StubStorageServer()
 
 
+class BrokenCopyable(Copyable):
+    """
+    I don't have a ``typeToCopy`` so I can't be serialized.
+    """
+
+
+@implementer(RIEcho)
+class Echoer(Referenceable):
+    def remote_echo(self, argument):
+        return argument
+
+
 @attr.s
 class DummyReferenceable(object):
     _interface = attr.ib()
@@ -103,18 +128,37 @@ class LocalRemote(object):
         """
         Call the given method on the wrapped object, passing the given arguments.
 
-        Arguments are checked for conformance to the remote interface but the
-        return value is not (because I don't know how -exarkun).
+        Arguments and return are checked for conformance to the remote
+        interface but they are not actually serialized.
 
         :return Deferred: The result of the call on the wrapped object.
         """
-        schema = self._referenceable.getInterface()[methname]
-        if self.check_args:
-            schema.checkAllArgs(args, kwargs, inbound=False)
-        # TODO: Figure out how to call checkResults on the result.
-        return execute(
-            self._referenceable.doRemoteCall,
-            methname,
-            args,
-            kwargs,
-        )
+        try:
+            schema = self._referenceable.getInterface()[methname]
+            if self.check_args:
+                schema.checkAllArgs(args, kwargs, inbound=True)
+            _check_copyables(list(args) + kwargs.values())
+            result = self._referenceable.doRemoteCall(
+                methname,
+                args,
+                kwargs,
+            )
+            schema.checkResults(result, inbound=False)
+            _check_copyables([result])
+            return succeed(result)
+        except:
+            return fail()
+
+
+def _check_copyables(copyables):
+    """
+    Check each object to see if it is a copyable and if it is make sure it can
+    be sliced.
+    """
+    for obj in copyables:
+        if ICopyable.providedBy(obj):
+            list(CopyableSlicer(obj).slice(False, None))
+        elif isinstance(obj, dict):
+            _check_copyables(obj.values())
+        elif isinstance(obj, list):
+            _check_copyables(obj)
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_foolscap.py b/src/_zkapauthorizer/tests/test_foolscap.py
index 388cc116fe71613c28419f87cdf6e2c4777e68c7..5912b3531c32e8b896834a77f88d66469aea0d85 100644
--- a/src/_zkapauthorizer/tests/test_foolscap.py
+++ b/src/_zkapauthorizer/tests/test_foolscap.py
@@ -20,16 +20,36 @@ from __future__ import (
     absolute_import,
 )
 
+from fixtures import (
+    Fixture,
+)
 from testtools import (
     TestCase,
 )
 from testtools.matchers import (
+    Equals,
     MatchesAll,
     AfterPreprocessing,
     Always,
     IsInstance,
 )
+from testtools.twistedsupport import (
+    succeeded,
+    failed,
+)
+
+from twisted.trial.unittest import (
+    TestCase as TrialTestCase,
+)
+from twisted.internet.defer import (
+    inlineCallbacks,
+)
 
+from foolscap.api import (
+    Violation,
+    RemoteInterface,
+    Any,
+)
 from foolscap.furl import (
     decode_furl,
 )
@@ -51,10 +71,27 @@ from hypothesis.strategies import (
 
 from .foolscap import (
     RIStub,
+    Echoer,
     LocalRemote,
+    BrokenCopyable,
     DummyReferenceable,
 )
 
+from ..foolscap import (
+    ShareStat,
+)
+
+class IHasSchema(RemoteInterface):
+    def method(arg=int):
+        return bytes
+
+    def good_method(arg=int):
+        return None
+
+    def whatever_method(arg=Any()):
+        return Any()
+
+
 def remote_reference():
     tub = Tub()
     tub.setLocation("127.0.0.1:12345")
@@ -95,3 +132,119 @@ class LocalRemoteTests(TestCase):
                 ),
             ),
         )
+
+    def test_arg_schema(self):
+        """
+        ``LocalRemote.callRemote`` returns a ``Deferred`` that fails with a
+        ``Violation`` if an parameter receives an argument which doesn't
+        conform to its schema.
+        """
+        ref = LocalRemote(DummyReferenceable(IHasSchema))
+        self.assertThat(
+            ref.callRemote("method", None),
+            failed(
+                AfterPreprocessing(
+                    lambda f: f.type,
+                    Equals(Violation),
+                ),
+            ),
+        )
+
+    def test_result_schema(self):
+        """
+        ``LocalRemote.callRemote`` returns a ``Deferred`` that fails with a
+        ``Violation`` if a method returns an object which doesn't conform to
+        the method's result schema.
+        """
+        ref = LocalRemote(DummyReferenceable(IHasSchema))
+        self.assertThat(
+            ref.callRemote("method", 0),
+            failed(
+                AfterPreprocessing(
+                    lambda f: f.type,
+                    Equals(Violation),
+                ),
+            ),
+        )
+
+    def test_successful_method(self):
+        """
+        ``LocalRemote.callRemote`` returns a ``Deferred`` that fires with the
+        remote method's result if the arguments and result conform to their
+        respective schemas.
+        """
+        ref = LocalRemote(DummyReferenceable(IHasSchema))
+        self.assertThat(
+            ref.callRemote("good_method", 0),
+            succeeded(Equals(None)),
+        )
+
+    def test_argument_serialization_failure(self):
+        """
+        ``LocalRemote.callRemote`` returns a ``Deferred`` that fires with a
+        failure if an argument cannot be serialized.
+        """
+        ref = LocalRemote(DummyReferenceable(IHasSchema))
+        self.assertThat(
+            ref.callRemote("whatever_method", BrokenCopyable()),
+            failed(Always()),
+        )
+
+    def test_result_serialization_failure(self):
+        """
+        ``LocalRemote.callRemote`` returns a ``Deferred`` that fires with a
+        failure if the method's result cannot be serialized.
+        """
+        class BrokenResultReferenceable(DummyReferenceable):
+            def doRemoteCall(self, *a, **kw):
+                return BrokenCopyable()
+
+        ref = LocalRemote(BrokenResultReferenceable(IHasSchema))
+        self.assertThat(
+            ref.callRemote("whatever_method", None),
+            failed(Always()),
+        )
+
+
+class EchoerFixture(Fixture):
+    def __init__(self, reactor, tub_path):
+        self.reactor = reactor
+        self.tub = Tub()
+        self.tub.setLocation(b"tcp:0")
+
+    def _setUp(self):
+        self.tub.startService()
+        self.furl = self.tub.registerReference(Echoer())
+
+    def _cleanUp(self):
+        return self.tub.stopService()
+
+
+class SerializationTests(TrialTestCase):
+    """
+    Tests for the serialization of types used in the Foolscap API.
+    """
+    def test_sharestat(self):
+        """
+        A ``ShareStat`` instance can be sent as an argument to and received in a
+        response from a Foolscap remote method call.
+        """
+        return self._roundtrip_test(ShareStat(1, 2))
+
+    @inlineCallbacks
+    def _roundtrip_test(self, obj):
+        """
+        Send ``obj`` over Foolscap and receive it back again, equal to itself.
+        """
+        # Foolscap Tub implementation just uses the global reactor...
+        from twisted.internet import reactor
+
+        # So sad.  No Deferred support in testtools.TestCase or
+        # fixture.Fixture, no fixture support in
+        # twisted.trial.unittest.TestCase.
+        fx = EchoerFixture(reactor, self.mktemp())
+        fx.setUp()
+        self.addCleanup(fx._cleanUp)
+        echoer = yield fx.tub.getReference(fx.furl)
+        received = yield echoer.callRemote("echo", obj)
+        self.assertEqual(obj, received)
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]),
         )