diff --git a/dist/pythonlibs/testrunner/__init__.py b/dist/pythonlibs/testrunner/__init__.py
index 9d16cb3df76622531e1cbc673b3e04e2b7cd29ec..9ff2d9de3bae346a1c80281181fd47c69a09abcd 100755
--- a/dist/pythonlibs/testrunner/__init__.py
+++ b/dist/pythonlibs/testrunner/__init__.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2017 Cenk Gündoğan <cenk.guendogan@haw-hamburg.de>
+# Copyright (C) 2018-19 Freie Universität Berlin
+#               2017 Cenk Gündoğan <cenk.guendogan@haw-hamburg.de>
 #               2016 Kaspar Schleiser <kaspar@schleiser.de>
 #               2014 Martine Lenders <mlenders@inf.fu-berlin.de>
 #
@@ -7,51 +8,17 @@
 # directory for more details.
 
 import os
-import signal
 import sys
-import subprocess
-import time
-from traceback import extract_tb, print_tb
-
+from traceback import print_tb
 import pexpect
 
-PEXPECT_PATH = os.path.dirname(pexpect.__file__)
-RIOTBASE = (os.environ.get('RIOTBASE') or
-            os.path.abspath(os.path.join(os.path.dirname(__file__),
-                                         "..", "..", "..")))
-
-# Setting an empty 'TESTRUNNER_START_DELAY' environment variable use the
-# default value (3)
-MAKE_TERM_STARTED_DELAY = int(os.environ.get('TESTRUNNER_START_DELAY') or 3)
-
-
-def list_until(l, cond):
-    return l[:([i for i, e in enumerate(l) if cond(e)][0])]
-
-
-def find_exc_origin(exc_info):
-    pos = list_until(extract_tb(exc_info),
-                     lambda frame: frame[0].startswith(PEXPECT_PATH)
-                     )[-1]
-    return (pos[3], os.path.relpath(os.path.abspath(pos[0]), RIOTBASE), pos[1])
+from .spawn import find_exc_origin, setup_child, teardown_child
+from .unittest import PexpectTestCase   # noqa, F401 expose to users
 
 
 def run(testfunc, timeout=10, echo=True, traceback=False):
-    env = os.environ.copy()
-    child = pexpect.spawnu("make term", env=env, timeout=timeout, codec_errors='replace')
-
-    # on many platforms, the termprog needs a short while to be ready...
-    time.sleep(MAKE_TERM_STARTED_DELAY)
-
-    if echo:
-        child.logfile = sys.stdout
-
-    try:
-        subprocess.check_output(('make', 'reset'), env=env,
-                                stderr=subprocess.PIPE)
-    except subprocess.CalledProcessError:
-        # make reset yields error on some boards even if successful
-        pass
+    child = setup_child(timeout, env=os.environ,
+                        logfile=sys.stdout if echo else None)
     try:
         testfunc(child)
     except pexpect.TIMEOUT:
@@ -62,16 +29,12 @@ def run(testfunc, timeout=10, echo=True, traceback=False):
         return 1
     except pexpect.EOF:
         trace = find_exc_origin(sys.exc_info()[2])
-        print("Unexpected end of file in expect script at \"%s\" (%s:%d)" % trace)
+        print("Unexpected end of file in expect script at \"%s\" (%s:%d)" %
+              trace)
         if traceback:
             print_tb(sys.exc_info()[2])
         return 1
     finally:
         print("")
-        try:
-            os.killpg(os.getpgid(child.pid), signal.SIGKILL)
-        except ProcessLookupError:
-            print("Process already stopped")
-
-        child.close()
+        teardown_child(child)
     return 0
diff --git a/dist/pythonlibs/testrunner/spawn.py b/dist/pythonlibs/testrunner/spawn.py
new file mode 100644
index 0000000000000000000000000000000000000000..7beab825d5338dfabd4ee5e7617eb13426b7e496
--- /dev/null
+++ b/dist/pythonlibs/testrunner/spawn.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2018-19 Freie Universität Berlin
+#               2017 Cenk Gündoğan <cenk.guendogan@haw-hamburg.de>
+#               2016 Kaspar Schleiser <kaspar@schleiser.de>
+#               2014 Martine Lenders <mlenders@inf.fu-berlin.de>
+#
+# This file is subject to the terms and conditions of the GNU Lesser
+# General Public License v2.1. See the file LICENSE in the top level
+# directory for more details.
+
+import os
+import pexpect
+import signal
+import subprocess
+import time
+from traceback import extract_tb
+
+PEXPECT_PATH = os.path.dirname(pexpect.__file__)
+RIOTBASE = (os.environ.get('RIOTBASE') or
+            os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                         "..", "..", "..")))
+
+# Setting an empty 'TESTRUNNER_START_DELAY' environment variable use the
+# default value (3)
+MAKE_TERM_STARTED_DELAY = int(os.environ.get('TESTRUNNER_START_DELAY') or 3)
+
+
+def list_until(l, cond):
+    return l[:([i for i, e in enumerate(l) if cond(e)][0])]
+
+
+def find_exc_origin(exc_info):
+    pos = list_until(extract_tb(exc_info),
+                     lambda frame: frame[0].startswith(PEXPECT_PATH)
+                     )[-1]
+    return (pos[3], os.path.relpath(os.path.abspath(pos[0]), RIOTBASE), pos[1])
+
+
+def setup_child(timeout=10, spawnclass=pexpect.spawnu, env=None, logfile=None):
+    child = spawnclass("make term", env=env, timeout=timeout,
+                       codec_errors='replace')
+
+    # on many platforms, the termprog needs a short while to be ready...
+    time.sleep(MAKE_TERM_STARTED_DELAY)
+
+    child.logfile = logfile
+
+    try:
+        subprocess.check_output(('make', 'reset'), env=env,
+                                stderr=subprocess.PIPE)
+    except subprocess.CalledProcessError:
+        # make reset yields error on some boards even if successful
+        pass
+    return child
+
+
+def teardown_child(child):
+    try:
+        os.killpg(os.getpgid(child.pid), signal.SIGKILL)
+    except ProcessLookupError:
+        print("Process already stopped")
+
+    child.close()
diff --git a/dist/pythonlibs/testrunner/unittest.py b/dist/pythonlibs/testrunner/unittest.py
new file mode 100644
index 0000000000000000000000000000000000000000..4ee7b25959db147d22d8456d9d31d138ebfc87dd
--- /dev/null
+++ b/dist/pythonlibs/testrunner/unittest.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2018-19 Freie Universität Berlin
+#
+# This file is subject to the terms and conditions of the GNU Lesser
+# General Public License v2.1. See the file LICENSE in the top level
+# directory for more details.
+
+import unittest
+from testrunner import setup_child, teardown_child
+
+
+class PexpectTestCase(unittest.TestCase):
+    TIMEOUT = 10
+    LOGFILE = None
+
+    """A unittest TestCase providing a pexpect spawn object to it's tests
+    """
+    @classmethod
+    def setUpClass(cls):
+        cls.spawn = setup_child(cls.TIMEOUT, logfile=cls.LOGFILE)
+
+    @classmethod
+    def tearDownClass(cls):
+        teardown_child(cls.spawn)