diff --git a/.hgignore b/.hgignore
--- a/.hgignore
+++ b/.hgignore
@@ -8,4 +8,5 @@ tmp/
dist/
django_conntrackt.egg-info/
projtect/.coverage
-test_binaries/
\ No newline at end of file
+test_binaries/
+geckodriver.log
\ No newline at end of file
diff --git a/testproject/functional_tests/__init__.py b/testproject/functional_tests/__init__.py
new file mode 100644
--- /dev/null
+++ b/testproject/functional_tests/__init__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2017 Branko Majic
+#
+# This file is part of Django Conntrackt.
+#
+# Django Conntrackt is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# Django Conntrackt is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Django Conntrackt. If not, see .
+#
diff --git a/testproject/functional_tests/base.py b/testproject/functional_tests/base.py
new file mode 100644
--- /dev/null
+++ b/testproject/functional_tests/base.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2017 Branko Majic
+#
+# This file is part of Django Conntrackt.
+#
+# Django Conntrackt is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# Django Conntrackt is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Django Conntrackt. If not, see .
+#
+
+
+# Standard library imports.
+import os
+import time
+
+# Django imports.
+from django.contrib.staticfiles.testing import StaticLiveServerTestCase
+
+# Third-party library imports.
+from selenium.common.exceptions import WebDriverException
+from selenium import webdriver
+
+
+# Maximum amount of time to wait before assuming a test has
+# failed. Used as default for functions that wait.
+MAX_WAIT = 10
+
+# Delay between subsequent runs of function when using the wait
+# decorator.
+EXECUTION_DELAY = 0.5
+
+# Base directory where the tests are located at.
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+
+# Base directory where the test binaries are located at.
+TEST_BINARIES_DIR = os.path.join(BASE_DIR, "..", "..", "test_binaries")
+
+# Location of Firefox binary.
+FIREFOX_BINARY = os.path.join(TEST_BINARIES_DIR, "firefox", "firefox")
+
+# Location of geckodriver binary.
+GECKODRIVER_BINARY = os.path.join(TEST_BINARIES_DIR, "geckodriver")
+
+
+def wait(fn, execution_delay=EXECUTION_DELAY, max_wait=MAX_WAIT):
+ """
+ Decorator that will produce a wrapper function that will run the
+ specified function repeatedly as long as all of the following
+ conditions have been met:
+
+ - Function throws either AssertionError or
+ selenium.common.exceptions.WebDriverException.
+ - Combined runtime of all function calls does not exceed the
+ maximum wait time.
+
+ As soon as the original function returns without any thrown
+ exceptions, the wrapper function will return its result.
+
+ If the function throws any other exception beyond the ones listed
+ above, this exception will get propagated.
+
+ Arguments:
+
+ fn
+ Function that should be run.
+
+ max_wait
+ Maximum time in seconds to wait for function to execute
+ successfully.
+
+ execution_delay
+ Delay in seconds between subsequent function runs. Used to
+ prevent high CPU usage.
+
+ Returns:
+
+ Wrapper function around passed-in function. The wrapper
+ function, provided it executes without failure, returns the
+ result of original function.
+ """
+
+ def modified_fn(*args, **kwargs):
+ start_time = time.time()
+
+ while True:
+ try:
+ return fn(*args, **kwargs)
+ except (AssertionError, WebDriverException) as e:
+ if time.time() - start_time > max_wait:
+ raise e
+ time.sleep(execution_delay)
+
+ return modified_fn
+
+
+class FunctionalTest(StaticLiveServerTestCase):
+ """
+ Helper test class that provides some convenience when writing
+ functional tests.
+
+ During test set-up, the class will:
+
+ - Set-up a browser instance, storing it in property self.browser.
+
+ During the test tear-down, the class will:
+
+ - Destroy the browser instance.
+
+ In addition to this, the class provides a convenience method
+ wait_for which can be used to wait for a function to execute
+ within a time slot and assert something, as described for the wait
+ decorator.
+ """
+
+ def setUp(self):
+ self.browser = webdriver.Firefox(firefox_binary=FIREFOX_BINARY, executable_path=GECKODRIVER_BINARY)
+
+ def tearDown(self):
+ self.browser.quit()
+ super(FunctionalTest, self).tearDown()
+
+ @wait
+ def wait_for(self, fn):
+ return fn()