diff --git a/.circleci/config.yml b/.circleci/config.yml
index f0a4bc18189e0001131f825dd902bb03540fc2bc..e1ac8d0e4924486996bf399b757e8b268bc42a0e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-version: 2
+version: 2.1
 
 aliases:
   - &PREPARE_VIRTUALENV
@@ -47,7 +47,93 @@ jobs:
           path: "docs/build"
           destination: "docs"
 
-  tests:
+  macos-tests:
+    parameters:
+      py-version:
+        type: "string"
+      xcode-version:
+        type: "string"
+
+    macos:
+      xcode: << parameters.xcode-version >>
+
+    steps:
+      - "checkout"
+
+      - restore_cache:
+          keys:
+          # when setup.cfg changes, use increasingly general patterns to
+          # restore cache
+          - pip-packages-v1-{{ .Branch }}-{{ checksum "setup.cfg" }}
+          - pip-packages-v1-{{ .Branch }}-
+          - pip-packages-v1-
+
+      - run:
+          name: "Get Pip"
+          command: |
+            # The CircleCI macOS environment has curl and Python but does not
+            # have pip.  So, for starters, use curl and Python to get pip.
+            if [ "<< parameters.py-version >>" == "2.7" ]; then
+              curl https://bootstrap.pypa.io/2.7/get-pip.py -o get-pip.py
+            else
+              curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
+            fi
+            python<< parameters.py-version >> get-pip.py
+
+      - run:
+          name: "Create Virtualenv"
+          command: |
+            # The CircleCI macOS Python environment has some Python libraries
+            # in it which conflict with ZKAPAuthorizer's requirements.  So
+            # install virtualenv and use it to create an environment for
+            # ZKAPAuthorizer so it can have all its own versions of its
+            # dependencies.
+            python<< parameters.py-version >> -m pip install virtualenv
+
+            # Make sure virtualenv creates a Python 2 environment!
+            virtualenv --python=python<< parameters.py-version >> venv
+
+            # Get the newest pip we can because who knows what came along with
+            # that virtualenv.
+            venv/bin/pip install --find-links file://${PWD}/wheelhouse --upgrade pip
+
+      - run:
+          name: "Populate Wheelhouse"
+          command: |
+            # Get wheels for all the Python packages we depend on - either
+            # directly via the ZKAPAuthorizer distutils configuration *or*
+            # because some other tool depends on it.  For example, pip has a
+            # bunch of dependencies it is nice to have around, and using the
+            # wheels depends on the wheel package.
+            venv/bin/pip wheel --wheel-dir wheelhouse pip wheel .[test]
+
+      - save_cache:
+          paths:
+          - "wheelhouse"
+          key: pip-packages-v1-{{ .Branch }}-{{ checksum "setup.cfg" }}
+
+      - run:
+          name: "Install"
+          command: |
+            # Now we can install ZKAPAuthorizer and its dependencies and test
+            # dependencies into the environment.
+            venv/bin/pip install --no-index --find-links file://${PWD}/wheelhouse .[test]
+
+      - run:
+          name: "Test"
+          command: |
+            # The test suite might leak file descriptors.  macOS defaults to a
+            # limit of 256.  This should be fixed, but not now ...
+            ulimit -Sn 1024
+            # And finally we can run the tests.  We'll run them with 4 jobs
+            # because the resource class documented at
+            # https://support.circleci.com/hc/en-us/articles/360009144794-macOS-resources
+            # says "Medium: 4 vCPUs, 8GB RAM".
+            venv/bin/python -m twisted.trial --jobs 4 --rterrors _zkapauthorizer
+          environment:
+            ZKAPAUTHORIZER_HYPOTHESIS_PROFILE: "ci"
+
+  linux-tests:
     docker:
       # Run in a highly Nix-capable environment.
       - image: "nixorg/nix:circleci"
@@ -142,5 +228,15 @@ workflows:
   version: 2
   everything:
     jobs:
-      - "documentation"
-      - "tests"
+    - "documentation"
+    - "linux-tests"
+    - "macos-tests":
+        matrix:
+          parameters:
+            py-version:
+            - "2.7"
+
+            xcode-version:
+            # https://circleci.com/docs/2.0/testing-ios/#supported-xcode-versions
+            - "12.3.0"
+            - "11.7.0"
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index c8995972397b0b4eee5e00b527d99897cf39050c..524b33218088ed1b9905334933e482312f3a69d9 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -41,15 +41,11 @@ jobs:
 
     - name: "Install CI Dependencies"
       run: |
-        python -m pip install -v wheel coverage
+        python -m pip install -v wheel
 
-    - name: "Install Test Dependencies"
+    - name: "Install ZKAPAuthorizer and dependencies"
       run: |
-        python -m pip install -v -r test-requirements.txt
-
-    - name: "Install ZKAPAuthorizer"
-      run: |
-        python -m pip install -v ./
+        python -m pip install -v ./[test]
 
     - name: "Dump Python Environment"
       run: |
diff --git a/setup.cfg b/setup.cfg
index 42dd1649cf7c1b05292f9e6f17647bec69143f59..ba14ff202611a712c8411bf50b935341c865ac9f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -8,8 +8,6 @@ keywords = tahoe-lafs, storage, privacy, cryptography
 license = Apache 2.0
 classifiers =
     Framework :: Twisted
-    Programming Language :: Python :: 3
-    Programming Language :: Python :: 3.7
     Programming Language :: Python :: 2
     Programming Language :: Python :: 2.7
 author = PrivateStorage.io, LLC
@@ -44,6 +42,9 @@ install_requires =
     treq
     pyutil
 
+[options.extras_require]
+test = coverage; fixtures; testtools; hypothesis
+
 [versioneer]
 VCS = git
 style = pep440
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index 12b0393b1eddc8869b55e759f4ebbfb99e03773a..7c62b8ff8e014b68c598822fb4a3b524fa83d38e 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -40,7 +40,7 @@ from sqlite3 import (
 import attr
 
 from aniso8601 import (
-    parse_datetime,
+    parse_datetime as _parse_datetime,
 )
 from twisted.logger import (
     Logger,
@@ -72,6 +72,18 @@ from .schema import (
 )
 
 
+def parse_datetime(s, **kw):
+    """
+    Like ``aniso8601.parse_datetime`` but accept unicode as well.
+    """
+    if isinstance(s, unicode):
+        s = s.encode("utf-8")
+    assert isinstance(s, bytes)
+    if "delimiter" in kw and isinstance(kw["delimiter"], unicode):
+        kw["delimiter"] = kw["delimiter"].encode("utf-8")
+    return _parse_datetime(s, **kw)
+
+
 class ILeaseMaintenanceObserver(Interface):
     """
     An object which is interested in receiving events related to the progress
diff --git a/test-requirements.txt b/test-requirements.txt
deleted file mode 100644
index 68868665b6a9b214a73a65c3376781d1eda91cc6..0000000000000000000000000000000000000000
--- a/test-requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-fixtures
-testtools
-hypothesis