diff --git a/.circleci/config.yml b/.circleci/config.yml
index 99fc0e672b05613d0aa10ee9d44bcfb2bdcb9cac..a6a03fad13922063ff2dee5faa7bfdeb2117fcb2 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -34,9 +34,31 @@ jobs:
           path: "docs/build"
           destination: "docs"
 
+  tests:
+    docker:
+      - image: "circleci/python:3.7"
+    steps:
+      - "checkout"
+
+      - run:
+        name: "Run Test Suite"
+        command: |
+            virtualenv venv
+            . venv/bin/activate
+            pip install --upgrade pip
+            pip install -r requirements.txt requirements-tests.txt
+            coverage run -m twisted.trial _secureaccesstokenauthorizer
+
+      - run:
+        name: "Report Coverage"
+        command: |
+          . venv/bin/activate
+          CODECOV_TOKEN="cc6e4697-4337-4506-88af-92b8f8ca6b22" codecov
+
 
 workflows:
   version: 2
   everything:
     jobs:
       - "documentation"
+      - "tests"
diff --git a/requirements-tests.txt b/requirements-tests.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a10336e4c06295c9924e922c9b5cea2c49286d1d
--- /dev/null
+++ b/requirements-tests.txt
@@ -0,0 +1,13 @@
+# Pinned version of all test-only dependencies, direct and transitive.  These
+# are in addition to the dependencies in requirements.txt.  This is the
+# configuration tested on CI.
+chardet==3.0.4
+codecov==2.0.15
+coverage==4.5.3
+idna==2.8
+requests==2.22.0
+urllib3==1.25.3
+
+# As an exception, certifi may float.  It contains time-sensitive data
+# (certificates).  Pinning it guarantees a failure eventually.
+certifi
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..54c2b2ed5fe9c2f2c145af6837aba9a72a129d96
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+# Pinned version of all dependencies, direct and transitive, needed at
+# runtime.  This is the configuration tested on CI.