diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..af201aa63322cdf2c10e05e0a5f6cb182f7975d2
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,54 @@
+run tests:
+  stage: test
+  image: python:3.10.7
+  variables:
+    PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
+  cache:
+    paths:
+      - .cache/pip/
+  script:
+    - apt-get update -y
+    - apt-get install -y make
+    - python -V
+    - pip install pipenv
+    - python -V
+    - pip install pipenv
+    - make initialize
+    - make test
+  coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
+  artifacts:
+    when: always
+    paths:
+      - results/unittests/coverage_report_html
+    reports:
+      junit: results/unittests/junit_report.xml
+      coverage_report:
+        coverage_format: cobertura
+        path: results/unittests/coverage_report.xml
+
+doc:
+  image: debian
+  script:
+    - apt-get update -y
+    - apt-get install -y make texlive-latex-base texlive-font-utils
+    - if [[ ! -e bin/doxygen ]]; then
+    -   apt-get install -y python3 build-essential git cmake flex bison
+    -   git clone --depth 1 -b Release_1_9_6 https://github.com/doxygen/doxygen.git
+    -   mkdir -p doxygen/build/ bin/
+    -   cd doxygen/build
+    -   cmake .. -DCMAKE_INSTALL_PREFIX=../../
+    -   make
+    -   make install
+    -   cd ../..
+    -   rm -fr doxygen
+    - fi
+    - PATH="$PWD/bin:$PATH" make doc
+  cache:
+    key: doxygen_1_9_6
+    paths:
+      - bin/doxygen
+  artifacts:
+    when: always
+    paths:
+      - doc/html
+      - doc/latex
diff --git a/Makefile b/Makefile
index c5812a373082be13dc087274b60864c0a88d2980..6949bf5e3ef5476c51a7f472ec0b3b2ce926f1f1 100644
--- a/Makefile
+++ b/Makefile
@@ -11,6 +11,9 @@ initialize: .init-done
 doc:
 	$(MAKE) -C doc
 
+test:
+	./run_tests.sh
+
 clean:
 	$(MAKE) -C doc clean
 
diff --git a/README.md b/README.md
index 850877f424da7c73a9e8df32e4e7001c290ae565..ce59cb868e4267079968a64beeeaef47bfdc754e 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,12 @@ make initialize
 make doc
 ```
 
+```sh
+# Run unit tests storing the results under
+#   $PWD/results/unittests
+make test
+```
+
 ```sh
 # Cleans intermediate files
 make clean
diff --git a/run_tests.sh b/run_tests.sh
new file mode 100755
index 0000000000000000000000000000000000000000..ef0c957692646c7055dea1f01a1543456141c8f0
--- /dev/null
+++ b/run_tests.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+## This file is part of the simulative evaluation for the qronos observer abstractions.
+## Copyright (C) 2022-2023  Tim Rheinfels  <tim.rheinfels@fau.de>
+## See https://gitlab.cs.fau.de/qronos-state-abstractions/simulation
+##
+## Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+##
+## 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+##
+## 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+##
+## 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+##
+## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+make initialize
+
+source config.sh
+
+export RESULT_DIR="$RESULT_DIR_BASE/unittests"
+mkdir -p $RESULT_DIR
+
+SPECIALISATION="${1:-}"
+
+if [[ -z "$SPECIALISATION" ]]; then
+    TESTUNITS="$(find src/test/ -name '*.py' -not -name __init__.py)"
+else
+    TESTUNITS="src/test/$SPECIALISATION"
+fi
+
+pipenv run coverage run --branch --data-file=$RESULT_DIR/.coverage -m pytest -v --junit-xml=$RESULT_DIR/junit_report.xml $TESTUNITS
+RESULT=$?
+
+pipenv run coverage report --data-file=$RESULT_DIR/.coverage || exit 1
+pipenv run coverage html --data-file=$RESULT_DIR/.coverage --directory=$RESULT_DIR/coverage_report_html || exit 1
+pipenv run coverage xml --data-file=$RESULT_DIR/.coverage -o $RESULT_DIR/coverage_report.xml || exit 1
+
+exit $RESULT
diff --git a/src/test/__init__.py b/src/test/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391