diff --git a/.github/workflows/lint-php.yml b/.github/workflows/lint-php.yml
new file mode 100644
index 0000000..8f5a47b
--- /dev/null
+++ b/.github/workflows/lint-php.yml
@@ -0,0 +1,30 @@
+name: Lint PHP
+on: [push, pull_request]
+
+jobs:
+  php-cs-fixer:
+    name: PHP-CS-Fixer
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@master
+      - name: Composer Action
+        run: docker run -v $PWD/:/app gone/php:cli-7.4 /usr/local/bin/composer install
+      - name: PHP-CS-Fixer
+        run: |
+          docker-compose run web \
+            phpdbg -qrr -d memory_limit=-1 \
+              vendor/bin/php-cs-fixer fix --dry-run
+  phpstan:
+    name: PHPStan
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@master
+      - name: Composer Action
+        run: docker run -v $PWD/:/app gone/php:cli-7.4 /usr/local/bin/composer install
+      - name: PHPStan
+        run: |
+          docker-compose run web \
+            phpdbg -qrr -d memory_limit=-1 \
+              vendor/bin/phpstan analyse src/ test/ bin
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
new file mode 100644
index 0000000..fdbca73
--- /dev/null
+++ b/.github/workflows/unit-tests.yml
@@ -0,0 +1,61 @@
+name: Test
+on: [push, pull_request]
+jobs:
+  ingest:
+    name: PHPUnit/Ingest
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@master
+      - name: Composer Install
+        run: docker run -v $PWD/:/app gone/php:cli-7.4 /usr/local/bin/composer install
+      - name: PHPUnit
+        run: |
+          docker-compose run web \
+            phpdbg -qrr -d memory_limit=-1 \
+              vendor/bin/paratest \
+                --testsuite=Ingest
+
+  human:
+    name: PHPUnit/Human
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@master
+      - name: Composer Install
+        run: docker run -v $PWD/:/app gone/php:cli-7.4 /usr/local/bin/composer install
+      - name: PHPUnit
+        run: |
+          docker-compose run test \
+            phpdbg -qrr -d memory_limit=-1 \
+              vendor/bin/phpunit \
+                --testsuite=Human
+
+  models:
+    name: PHPUnit/Models
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@master
+      - name: Composer Install
+        run: docker run -v $PWD/:/app gone/php:cli-7.4 /usr/local/bin/composer install
+      - name: PHPUnit
+        run: |
+          docker-compose run test \
+            phpdbg -qrr -d memory_limit=-1 \
+              vendor/bin/phpunit \
+                --testsuite=Models
+  services:
+    name: PHPUnit/Services
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@master
+      - name: Composer Install
+        run: docker run -v $PWD/:/app gone/php:cli-7.4 /usr/local/bin/composer install
+      - name: PHPUnit
+        run: |
+          docker-compose run test \
+            phpdbg -qrr -d memory_limit=-1 \
+              vendor/bin/phpunit \
+                --testsuite=Services
diff --git a/.gitignore b/.gitignore
index e30f107..1c2f231 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,7 @@ vendor/
 .php_cs.cache
 docs
 phploc.xml
-cghooks.lock
\ No newline at end of file
+cghooks.lock
+/.php-cs-fixer.cache
+/.coverage
+/phpunit.xml
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 43249f1..e7532cd 100644
--- a/composer.json
+++ b/composer.json
@@ -13,7 +13,7 @@
     "sort-packages": true
   },
   "require": {
-    "php": ">=8.0",
+    "php": ">=8.2",
     "ext-apcu": "*",
     "ext-curl": "*",
     "ext-iconv": "*",
@@ -62,6 +62,7 @@
     "slim/twig-view": "^3.2",
     "squizlabs/php_codesniffer": "3.*",
     "swaggest/json-schema": "^0.12.39",
+    "symfony/polyfill-intl-icu": "^1.29",
     "symfony/translation": "^5.1",
     "symfony/twig-bridge": "^5.1",
     "symfony/yaml": "^5.1",
@@ -91,7 +92,11 @@
   },
   "autoload": {
     "psr-4": {
-      "Benzine\\": "src",
+      "Benzine\\": "src/"
+    }
+  },
+  "autoload-dev": {
+    "psr-4": {
       "Benzine\\Tests\\": "tests/"
     }
   },
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..2816c7b
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd"
+         bootstrap="tests/bootstrap.php"
+         enforceTimeLimit="true"
+         defaultTimeLimit="3"
+         executionOrder="depends,defects"
+         cacheResult="true"
+         cacheResultFile=".phpunit.result.cache"
+         testdox="true"
+         colors="true"
+         failOnRisky="true"
+         failOnWarning="true"
+         failOnIncomplete="false"
+         failOnSkipped="false"
+         failOnDeprecation="true"
+         failOnEmptyTestSuite="true"
+         failOnNotice="true"
+         displayDetailsOnSkippedTests="true"
+         displayDetailsOnTestsThatTriggerDeprecations="true"
+         displayDetailsOnTestsThatTriggerErrors="true"
+         displayDetailsOnTestsThatTriggerNotices="true"
+         displayDetailsOnTestsThatTriggerWarnings="true"
+         beStrictAboutOutputDuringTests="true"
+         stopOnDefect="false"
+>
+    <php>
+        <ini name="memory_limit" value="128M" />
+        <ini name="display_errors" value="On"/>
+        <ini name="display_startup_errors" value="On"/>
+        <ini name="error_reporting" value="E_ALL"/>
+        <env name="BENZINE_CONFIG_PATH" value="tests/.benzine.yml"/>
+        <env name="XDEBUG_MODE" value="coverage"/>
+    </php>
+    <coverage includeUncoveredFiles="true" cacheDirectory=".coverage/cache">
+        <report>
+            <clover outputFile=".coverage/clover.xml"/>
+            <html outputDirectory=".coverage/html"/>
+            <text outputFile="php://stdout" showOnlySummary="true"/>
+        </report>
+    </coverage>
+    <extensions>
+        <bootstrap class="Ergebnis\PHPUnit\SlowTestDetector\Extension"/>
+    </extensions>
+    <testsuites>
+        <testsuite name="Test Suite">
+            <directory>./tests/</directory>
+        </testsuite>
+    </testsuites>
+    <source>
+        <include>
+            <directory suffix=".php">src</directory>
+        </include>
+        <exclude>
+            <directory suffix=".php">src/Fixtures</directory>
+        </exclude>
+    </source>
+</phpunit>
diff --git a/src/Services/EnvironmentService.php b/src/Services/EnvironmentService.php
index 34e3dcd..5f781a7 100644
--- a/src/Services/EnvironmentService.php
+++ b/src/Services/EnvironmentService.php
@@ -26,7 +26,7 @@ class EnvironmentService
         return $this->environmentVariables;
     }
 
-    public function get(string $key, ?string $default = null)
+    public function get(string $key, mixed $default = null)
     {
         if (isset($this->environmentVariables[$key])) {
             return $this->environmentVariables[$key];
diff --git a/src/Services/SessionService.php b/src/Services/SessionService.php
index 16f2424..014a113 100644
--- a/src/Services/SessionService.php
+++ b/src/Services/SessionService.php
@@ -55,24 +55,24 @@ class SessionService implements \SessionHandlerInterface
         session_start();
     }
 
-    public function close()
+    public function close(): bool
     {
         return true;
     }
 
-    public function destroy($session_id)
+    public function destroy(string $id): bool
     {
-        $this->oldID = $session_id;
+        $this->oldID = $id;
 
         return true;
     }
 
-    public function gc($maxlifetime)
+    public function gc(int $max_lifetime): false | int
     {
-        return true;
+        return 0;
     }
 
-    public function open($save_path, $name)
+    public function open(string $path, string $name): bool
     {
         return true;
     }
@@ -86,25 +86,23 @@ class SessionService implements \SessionHandlerInterface
         return $this->redisIsAvailable;
     }
 
-    public function read($session_id)
+    public function read(string $id): false | string
     {
         if ($this->useAPCU()) {
-            if (apcu_exists('read-' . $session_id)) {
-                return apcu_fetch('read-' . $session_id);
+            if (apcu_exists('read-' . $id)) {
+                return apcu_fetch('read-' . $id);
             }
         }
 
-        if (!empty($this->oldID)) {
-            $session_id = $this->oldID ? $this->oldID : $session_id;
-        }
+        $id = !empty($this->oldID) ? $this->oldID : $id;
 
         $result = '';
         if ($this->useRedis()) {
-            $serialised = $this->redis->get("session:{$session_id}");
+            $serialised = $this->redis->get("session:{$id}");
             if (null != $serialised) {
                 if (!empty($this->oldID)) {
                     // clean up old session after regenerate
-                    $this->redis->del("session:{$session_id}");
+                    $this->redis->del("session:{$id}");
                     $this->oldID = null;
                 }
                 $result = unserialize($serialised);
@@ -112,9 +110,9 @@ class SessionService implements \SessionHandlerInterface
         }
 
         if ($this->useAPCU()) {
-            apcu_store('read-' . $session_id, $result, 30);
+            apcu_store('read-' . $id, $result, 30);
         } else {
-            $this->dirtyCheck['read-' . $session_id] = crc32($result);
+            $this->dirtyCheck['read-' . $id] = crc32($result);
         }
 
         return $result;
diff --git a/tests/.benzine.yml b/tests/.benzine.yml
new file mode 100644
index 0000000..78d4250
--- /dev/null
+++ b/tests/.benzine.yml
@@ -0,0 +1,6 @@
+application:
+  name: Core Self Test App
+  core: Benzine\Tests\TestApp
+  default_access: public
+  debug: true
+  root: /app/tests
diff --git a/tests/AbstractCoreTest.php b/tests/AbstractCoreTest.php
new file mode 100644
index 0000000..c0c3471
--- /dev/null
+++ b/tests/AbstractCoreTest.php
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Benzine\Tests;
+
+use Benzine\App;
+
+abstract class AbstractCoreTest extends AbstractTestCase
+{
+    protected App $app;
+
+    public function setUp(): void
+    {
+        parent::setUp();
+        $this->app = new TestApp();
+    }
+}
diff --git a/tests/Dependencies/Monolog/MonologTest.php b/tests/Dependencies/Monolog/MonologTest.php
new file mode 100644
index 0000000..f71813b
--- /dev/null
+++ b/tests/Dependencies/Monolog/MonologTest.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Benzine\Tests\Dependencies\Monolog;
+
+use Benzine\Tests\AbstractCoreTest;
+use Monolog\Logger;
+use Psr\Log\LoggerInterface;
+
+/**
+ * @internal
+ *
+ * @coversNothing
+ */
+class MonologTest extends AbstractCoreTest
+{
+    public function testMonolog(): void
+    {
+        $logger = $this->app->get(Logger::class);
+        $this->assertInstanceOf(LoggerInterface::class, $logger);
+    }
+}
diff --git a/tests/TestApp.php b/tests/TestApp.php
new file mode 100644
index 0000000..6e33ef0
--- /dev/null
+++ b/tests/TestApp.php
@@ -0,0 +1,9 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Benzine\Tests;
+
+use Benzine\App;
+
+class TestApp extends App {}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..e895da4
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,8 @@
+<?php
+
+declare(strict_types=1);
+
+ini_set('xdebug.mode=coverage', 'on');
+define('APP_ROOT', __DIR__ . '/..');
+
+require_once APP_ROOT . '/vendor/autoload.php';