diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000000000000000000000000000000000000..1945bd4cd7f8193cf0f2e9029f82baa05a0146d7
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,10 @@
+[run]
+branch = true
+source =
+    pirates
+    tests
+parallel = true
+
+[report]
+show_missing = true
+omit = *migrations/*, tests/*
diff --git a/tests/requirements.txt b/tests/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e2f939e9e9cadcc1d972bda9c53d66ae966033b7
--- /dev/null
+++ b/tests/requirements.txt
@@ -0,0 +1,4 @@
+pytest
+pytest-cov
+pytest-factoryboy
+pytest-django
diff --git a/tests/settings.py b/tests/settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..80fdb2d8fb6670459363cfc00d42c30a8fe59c80
--- /dev/null
+++ b/tests/settings.py
@@ -0,0 +1,26 @@
+SECRET_KEY = "can you keep a secret?"
+
+DEBUG = True
+
+USE_TZ = True
+
+CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache",}}
+
+DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3",}}
+
+ROOT_URLCONF = "pirates.urls"
+
+INSTALLED_APPS = [
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.sites",
+    "mozilla_django_oidc",
+    "pirates",
+]
+
+SESSION_ENGINE = "django.contrib.sessions.backends.cache"
+
+MIDDLEWARE = []
+
+OIDC_USERNAME_ALGO = None
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000000000000000000000000000000000000..1f0abc05033e805998153f700f5daac9df8603f6
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,16 @@
+[tox]
+envlist =
+    py{36,37,38}-django300
+
+[testenv]
+deps =
+    -r{toxinidir}/tests/requirements.txt
+    django300: Django>=3.0.0,<3.1
+setenv =
+    PYTHONPATH = {toxinidir}
+    PYTHONUNBUFFERED = yes
+passenv =
+    *
+usedevelop = false
+commands =
+    {posargs:pytest --cov -vv tests}