diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..e8d15f4
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+indent_size = 2
diff --git a/.gitignore b/.gitignore
index a5ec20f..6014b45 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,8 @@
 vendor/
 composer.phar
 composer.lock
+.phpunit.result.cache
+coverage/
+.vscode/settings.json
+*.sublime-workspace
+.vscode/
\ No newline at end of file
diff --git a/README.md b/README.md
index 61083e9..daae451 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,28 @@
+
+
+
+[](http://hits.dwyl.com/flightphp/core)
+
+# What the fork?
+This is a fork of the original project [https://github.com/mikecao/flight](https://github.com/mikecao/flight). That project hasn't seen updates in quite some time, so this fork is to help maintain the project going forward.
+
 # What is Flight?
 
-Flight is a fast, simple, extensible framework for PHP. Flight enables you to 
+Flight is a fast, simple, extensible framework for PHP. Flight enables you to
 quickly and easily build RESTful web applications.
 
+Chat with us on Matrix IRC [#flight-php-framework:matrix.org](https://matrix.to/#/#flight-php-framework:matrix.org)
+
+# Basic Usage
 ```php
-require 'flight/Flight.php';
 
-Flight::route('/', function(){
-    echo 'hello world!';
+// if installed with composer
+require 'vendor/autoload.php';
+// or if installed manually by zip file
+//require 'flight/Flight.php';
+
+Flight::route('/', function() {
+  echo 'hello world!';
 });
 
 Flight::start();
@@ -27,14 +42,15 @@ Flight is released under the [MIT](http://flightphp.com/license) license.
 
 1\. Download the files.
 
-If you're using [Composer](https://getcomposer.org/), you can run the following command:
+If you're using [Composer](https://getcomposer.org/), you can run the following
+command:
 
-```
-composer require mikecao/flight
+```bash
+composer require n0nag0n/flight
 ```
 
-OR you can [download](https://github.com/mikecao/flight/archive/master.zip) them directly 
-and extract them to your web directory.
+OR you can [download](https://github.com/n0nag0n/flight/archive/master.zip)
+them directly and extract them to your web directory.
 
 2\. Configure your webserver.
 
@@ -47,15 +63,16 @@ RewriteCond %{REQUEST_FILENAME} !-d
 RewriteRule ^(.*)$ index.php [QSA,L]
 ```
 
-**Note**: If you need to use flight in a subdirectory add the line `RewriteBase /subdir/` just after `RewriteEngine On`.
+**Note**: If you need to use flight in a subdirectory add the line
+`RewriteBase /subdir/` just after `RewriteEngine On`.
 
 For *Nginx*, add the following to your server declaration:
 
 ```
 server {
-    location / {
-        try_files $uri $uri/ /index.php;
-    }
+  location / {
+  try_files $uri $uri/ /index.php;
+  }
 }
 ```
 3\. Create your `index.php` file.
@@ -75,8 +92,8 @@ require 'vendor/autoload.php';
 Then define a route and assign a function to handle the request.
 
 ```php
-Flight::route('/', function(){
-    echo 'hello world!';
+Flight::route('/', function () {
+  echo 'hello world!';
 });
 ```
 
@@ -91,16 +108,16 @@ Flight::start();
 Routing in Flight is done by matching a URL pattern with a callback function.
 
 ```php
-Flight::route('/', function(){
-    echo 'hello world!';
+Flight::route('/', function () {
+  echo 'hello world!';
 });
 ```
 
 The callback can be any object that is callable. So you can use a regular function:
 
 ```php
-function hello(){
-    echo 'hello world!';
+function hello() {
+  echo 'hello world!';
 }
 
 Flight::route('/', 'hello');
@@ -110,9 +127,9 @@ Or a class method:
 
 ```php
 class Greeting {
-    public static function hello() {
-        echo 'hello world!';
-    }
+  public static function hello() {
+    echo 'hello world!';
+  }
 }
 
 Flight::route('/', array('Greeting', 'hello'));
@@ -123,18 +140,18 @@ Or an object method:
 ```php
 class Greeting
 {
-    public function __construct() {
-        $this->name = 'John Doe';
-    }
+  public function __construct() {
+    $this->name = 'John Doe';
+  }
 
-    public function hello() {
-        echo "Hello, {$this->name}!";
-    }
+  public function hello() {
+    echo "Hello, {$this->name}!";
+  }
 }
 
 $greeting = new Greeting();
 
-Flight::route('/', array($greeting, 'hello')); 
+Flight::route('/', array($greeting, 'hello'));
 ```
 
 Routes are matched in the order they are defined. The first route to match a
@@ -146,20 +163,20 @@ By default, route patterns are matched against all request methods. You can resp
 to specific methods by placing an identifier before the URL.
 
 ```php
-Flight::route('GET /', function(){
-    echo 'I received a GET request.';
+Flight::route('GET /', function () {
+  echo 'I received a GET request.';
 });
 
-Flight::route('POST /', function(){
-    echo 'I received a POST request.';
+Flight::route('POST /', function () {
+  echo 'I received a POST request.';
 });
 ```
 
 You can also map multiple methods to a single callback by using a `|` delimiter:
 
 ```php
-Flight::route('GET|POST /', function(){
-    echo 'I received either a GET or a POST request.';
+Flight::route('GET|POST /', function () {
+  echo 'I received either a GET or a POST request.';
 });
 ```
 
@@ -168,8 +185,8 @@ Flight::route('GET|POST /', function(){
 You can use regular expressions in your routes:
 
 ```php
-Flight::route('/user/[0-9]+', function(){
-    // This will match /user/1234
+Flight::route('/user/[0-9]+', function () {
+  // This will match /user/1234
 });
 ```
 
@@ -179,8 +196,8 @@ You can specify named parameters in your routes which will be passed along to
 your callback function.
 
 ```php
-Flight::route('/@name/@id', function($name, $id){
-    echo "hello, $name ($id)!";
+Flight::route('/@name/@id', function(string $name, string $id) {
+  echo "hello, $name ($id)!";
 });
 ```
 
@@ -188,9 +205,9 @@ You can also include regular expressions with your named parameters by using
 the `:` delimiter:
 
 ```php
-Flight::route('/@name/@id:[0-9]{3}', function($name, $id){
-    // This will match /bob/123
-    // But will not match /bob/12345
+Flight::route('/@name/@id:[0-9]{3}', function(string $name, string $id) {
+  // This will match /bob/123
+  // But will not match /bob/12345
 });
 ```
 
@@ -202,13 +219,16 @@ You can specify named parameters that are optional for matching by wrapping
 segments in parentheses.
 
 ```php
-Flight::route('/blog(/@year(/@month(/@day)))', function($year, $month, $day){
+Flight::route(
+  '/blog(/@year(/@month(/@day)))',
+  function(?string $year, ?string $month, ?string $day) {
     // This will match the following URLS:
     // /blog/2012/12/10
     // /blog/2012/12
     // /blog/2012
     // /blog
-});
+  }
+);
 ```
 
 Any optional parameters that are not matched will be passed in as NULL.
@@ -219,16 +239,16 @@ Matching is only done on individual URL segments. If you want to match multiple
 segments you can use the `*` wildcard.
 
 ```php
-Flight::route('/blog/*', function(){
-    // This will match /blog/2000/02/01
+Flight::route('/blog/*', function () {
+  // This will match /blog/2000/02/01
 });
 ```
 
 To route all requests to a single callback, you can do:
 
 ```php
-Flight::route('*', function(){
-    // Do something
+Flight::route('*', function () {
+  // Do something
 });
 ```
 
@@ -238,16 +258,16 @@ You can pass execution on to the next matching route by returning `true` from
 your callback function.
 
 ```php
-Flight::route('/user/@name', function($name){
-    // Check some condition
-    if ($name != "Bob") {
-        // Continue to next route
-        return true;
-    }
+Flight::route('/user/@name', function (string $name) {
+  // Check some condition
+  if ($name != "Bob") {
+    // Continue to next route
+    return true;
+  }
 });
 
-Flight::route('/user/*', function(){
-    // This will get called
+Flight::route('/user/*', function () {
+  // This will get called
 });
 ```
 
@@ -259,18 +279,18 @@ the route method. The route object will always be the last parameter passed to y
 callback function.
 
 ```php
-Flight::route('/', function($route){
-    // Array of HTTP methods matched against
-    $route->methods;
+Flight::route('/', function(\flight\net\Route $route) {
+  // Array of HTTP methods matched against
+  $route->methods;
 
-    // Array of named parameters
-    $route->params;
+  // Array of named parameters
+  $route->params;
 
-    // Matching regular expression
-    $route->regex;
+  // Matching regular expression
+  $route->regex;
 
-    // Contains the contents of any '*' used in the URL pattern
-    $route->splat;
+  // Contains the contents of any '*' used in the URL pattern
+  $route->splat;
 }, true);
 ```
 
@@ -286,8 +306,8 @@ To map your own custom method, you use the `map` function:
 
 ```php
 // Map your method
-Flight::map('hello', function($name){
-    echo "hello $name!";
+Flight::map('hello', function ($name) {
+  echo "hello $name!";
 });
 
 // Call your custom method
@@ -318,7 +338,7 @@ Flight::register('db', 'PDO', array('mysql:host=localhost;dbname=test','user','p
 // Get an instance of your class
 // This will create an object with the defined parameters
 //
-//     new PDO('mysql:host=localhost;dbname=test','user','pass');
+// new PDO('mysql:host=localhost;dbname=test','user','pass');
 //
 $db = Flight::db();
 ```
@@ -329,8 +349,8 @@ new object. The callback function takes one parameter, an instance of the new ob
 
 ```php
 // The callback will be passed the object that was constructed
-Flight::register('db', 'PDO', array('mysql:host=localhost;dbname=test','user','pass'), function($db){
-    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+Flight::register('db', 'PDO', array('mysql:host=localhost;dbname=test','user','pass'), function (PDO $db): void {
+  $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 });
 ```
 
@@ -359,8 +379,8 @@ by using the `map` method:
 
 ```php
 Flight::map('notFound', function(){
-    // Display custom 404 page
-    include 'errors/404.html';
+  // Display custom 404 page
+  include 'errors/404.html';
 });
 ```
 
@@ -387,8 +407,8 @@ methods as well as any custom methods that you've mapped.
 A filter function looks like this:
 
 ```php
-function(&$params, &$output) {
-    // Filter code
+function (array &$params, string &$output) {
+  // Filter code
 }
 ```
 
@@ -397,16 +417,16 @@ Using the passed in variables you can manipulate the input parameters and/or the
 You can have a filter run before a method by doing:
 
 ```php
-Flight::before('start', function(&$params, &$output){
-    // Do something
+Flight::before('start', function (array &$params, string &$output) {
+  // Do something
 });
 ```
 
 You can have a filter run after a method by doing:
 
 ```php
-Flight::after('start', function(&$params, &$output){
-    // Do something
+Flight::after('start', function (array &$params, string &$output) {
+  // Do something
 });
 ```
 
@@ -417,20 +437,20 @@ Here's an example of the filtering process:
 
 ```php
 // Map a custom method
-Flight::map('hello', function($name){
-    return "Hello, $name!";
+Flight::map('hello', function ($name) {
+  return "Hello, $name!";
 });
 
 // Add a before filter
-Flight::before('hello', function(&$params, &$output){
-    // Manipulate the parameter
-    $params[0] = 'Fred';
+Flight::before('hello', function (array &$params, string &$output) {
+  // Manipulate the parameter
+  $params[0] = 'Fred';
 });
 
 // Add an after filter
-Flight::after('hello', function(&$params, &$output){
-    // Manipulate the output
-    $output .= " Have a nice day!";
+Flight::after('hello', function (array &$params, string &$output) {
+  // Manipulate the output
+  $output .= " Have a nice day!";
 });
 
 // Invoke the custom method
@@ -439,26 +459,28 @@ echo Flight::hello('Bob');
 
 This should display:
 
-    Hello Fred! Have a nice day!
+```
+Hello Fred! Have a nice day!
+```
 
 If you have defined multiple filters, you can break the chain by returning `false`
 in any of your filter functions:
 
 ```php
-Flight::before('start', function(&$params, &$output){
-    echo 'one';
+Flight::before('start', function (array &$params, string &$output){
+  echo 'one';
 });
 
-Flight::before('start', function(&$params, &$output){
-    echo 'two';
+Flight::before('start', function (array &$params, string &$output): bool {
+  echo 'two';
 
-    // This will end the chain
-    return false;
+  // This will end the chain
+  return false;
 });
 
 // This will not get called
-Flight::before('start', function(&$params, &$output){
-    echo 'three';
+Flight::before('start', function (array &$params, string &$output){
+  echo 'three';
 });
 ```
 
@@ -480,7 +502,7 @@ To see if a variable has been set you can do:
 
 ```php
 if (Flight::has('id')) {
-     // Do something
+  // Do something
 }
 ```
 
@@ -515,12 +537,14 @@ be reference like a local variable. Template files are simply PHP files. If the
 content of the `hello.php` template file is:
 
 ```php
-Hello, ''!
+Hello, !
 ```
 
 The output would be:
 
-    Hello, Bob!
+```
+Hello, Bob!
+```
 
 You can also manually set view variables by using the set method:
 
@@ -580,26 +604,26 @@ If the template files looks like this:
 
 ```php
 
-
-
-
-
-
-
-
+  
+    
+  
+  
+    
+    
+  
 
 ```
 
 The output would be:
 ```html
 
-
-Home Page
-
-
-Hello
-World
-
+  
+    Home Page
+  
+  
+    Hello
+    World
+  
 
 ```
 
@@ -615,11 +639,11 @@ require './Smarty/libs/Smarty.class.php';
 
 // Register Smarty as the view class
 // Also pass a callback function to configure Smarty on load
-Flight::register('view', 'Smarty', array(), function($smarty){
-    $smarty->template_dir = './templates/';
-    $smarty->compile_dir = './templates_c/';
-    $smarty->config_dir = './config/';
-    $smarty->cache_dir = './cache/';
+Flight::register('view', 'Smarty', array(), function (Smarty $smarty) {
+  $smarty->setTemplateDir() = './templates/';
+  $smarty->setCompileDir() = './templates_c/';
+  $smarty->setConfigDir() = './config/';
+  $smarty->setCacheDir() = './cache/';
 });
 
 // Assign template data
@@ -633,8 +657,8 @@ For completeness, you should also override Flight's default render method:
 
 ```php
 Flight::map('render', function($template, $data){
-    Flight::view()->assign($data);
-    Flight::view()->display($template);
+  Flight::view()->assign($data);
+  Flight::view()->display($template);
 });
 ```
 # Error Handling
@@ -648,9 +672,9 @@ response with some error information.
 You can override this behavior for your own needs:
 
 ```php
-Flight::map('error', function(Exception $ex){
-    // Handle error
-    echo $ex->getTraceAsString();
+Flight::map('error', function(Throwable $ex){
+  // Handle error
+  echo $ex->getTraceAsString();
 });
 ```
 
@@ -669,8 +693,8 @@ behavior is to send an `HTTP 404 Not Found` response with a simple message.
 You can override this behavior for your own needs:
 
 ```php
-Flight::map('notFound', function(){
-    // Handle not found
+Flight::map('notFound', function () {
+  // Handle not found
 });
 ```
 
@@ -701,26 +725,24 @@ $request = Flight::request();
 
 The request object provides the following properties:
 
-```
-url - The URL being requested
-base - The parent subdirectory of the URL
-method - The request method (GET, POST, PUT, DELETE)
-referrer - The referrer URL
-ip - IP address of the client
-ajax - Whether the request is an AJAX request
-scheme - The server protocol (http, https)
-user_agent - Browser information
-type - The content type
-length - The content length
-query - Query string parameters
-data - Post data or JSON data
-cookies - Cookie data
-files - Uploaded files
-secure - Whether the connection is secure
-accept - HTTP accept parameters
-proxy_ip - Proxy IP address of the client
-host - The request host name
-```
+- **url** - The URL being requested
+- **base** - The parent subdirectory of the URL
+- **method** - The request method (GET, POST, PUT, DELETE)
+- **referrer** - The referrer URL
+- **ip** - IP address of the client
+- **ajax** - Whether the request is an AJAX request
+- **scheme** - The server protocol (http, https)
+- **user_agent** - Browser information
+- **type** - The content type
+- **length** - The content length
+- **query** - Query string parameters
+- **data** - Post data or JSON data
+- **cookies** - Cookie data
+- **files** - Uploaded files
+- **secure** - Whether the connection is secure
+- **accept** - HTTP accept parameters
+- **proxy_ip** - Proxy IP address of the client
+- **host** - The request host name
 
 You can access the `query`, `data`, `cookies`, and `files` properties
 as arrays or objects.
@@ -739,7 +761,8 @@ $id = Flight::request()->query->id;
 
 ## RAW Request Body
 
-To get the raw HTTP request body, for example when dealing with PUT requests, you can do:
+To get the raw HTTP request body, for example when dealing with PUT requests,
+you can do:
 
 ```php
 $body = Flight::request()->getBody();
@@ -747,8 +770,8 @@ $body = Flight::request()->getBody();
 
 ## JSON Input
 
-If you send a request with the type `application/json` and the data `{"id": 123}` it will be available
-from the `data` property:
+If you send a request with the type `application/json` and the data `{"id": 123}`
+it will be available from the `data` property:
 
 ```php
 $id = Flight::request()->data->id;
@@ -768,9 +791,9 @@ and time a page was last modified. The client will continue to use their cache u
 the last modified value is changed.
 
 ```php
-Flight::route('/news', function(){
-    Flight::lastModified(1234567890);
-    echo 'This content will be cached.';
+Flight::route('/news', function () {
+  Flight::lastModified(1234567890);
+  echo 'This content will be cached.';
 });
 ```
 
@@ -780,9 +803,9 @@ Flight::route('/news', function(){
 want for the resource:
 
 ```php
-Flight::route('/news', function(){
-    Flight::etag('my-unique-id');
-    echo 'This content will be cached.';
+Flight::route('/news', function () {
+  Flight::etag('my-unique-id');
+  echo 'This content will be cached.';
 });
 ```
 
@@ -847,12 +870,12 @@ Flight::set('flight.log_errors', true);
 
 The following is a list of all the available configuration settings:
 
-    flight.base_url - Override the base url of the request. (default: null)
-    flight.case_sensitive - Case sensitive matching for URLs. (default: false)
-    flight.handle_errors - Allow Flight to handle all errors internally. (default: true)
-    flight.log_errors - Log errors to the web server's error log file. (default: false)
-    flight.views.path - Directory containing view template files. (default: ./views)
-    flight.views.extension - View template file extension. (default: .php)
+- **flight.base_url** - Override the base url of the request. (default: null)
+- **flight.case_sensitive** - Case sensitive matching for URLs. (default: false)
+- **flight.handle_errors** - Allow Flight to handle all errors internally. (default: true)
+- **flight.log_errors** - Log errors to the web server's error log file. (default: false)
+- **flight.views.path** - Directory containing view template files. (default: ./views)
+- **flight.views.extension** - View template file extension. (default: .php)
 
 # Framework Methods
 
@@ -864,15 +887,15 @@ or overridden.
 ## Core Methods
 
 ```php
-Flight::map($name, $callback) // Creates a custom framework method.
-Flight::register($name, $class, [$params], [$callback]) // Registers a class to a framework method.
-Flight::before($name, $callback) // Adds a filter before a framework method.
-Flight::after($name, $callback) // Adds a filter after a framework method.
-Flight::path($path) // Adds a path for autoloading classes.
-Flight::get($key) // Gets a variable.
-Flight::set($key, $value) // Sets a variable.
-Flight::has($key) // Checks if a variable is set.
-Flight::clear([$key]) // Clears a variable.
+Flight::map(string $name, callable $callback, bool $pass_route = false) // Creates a custom framework method.
+Flight::register(string $name, string $class, array $params = [], ?callable $callback = null) // Registers a class to a framework method.
+Flight::before(string $name, callable $callback) // Adds a filter before a framework method.
+Flight::after(string $name, callable $callback) // Adds a filter after a framework method.
+Flight::path(string $path) // Adds a path for autoloading classes.
+Flight::get(string $key) // Gets a variable.
+Flight::set(string $key, mixed $value) // Sets a variable.
+Flight::has(string $key) // Checks if a variable is set.
+Flight::clear(array|string $key = []) // Clears a variable.
 Flight::init() // Initializes the framework to its default settings.
 Flight::app() // Gets the application object instance
 ```
@@ -882,16 +905,16 @@ Flight::app() // Gets the application object instance
 ```php
 Flight::start() // Starts the framework.
 Flight::stop() // Stops the framework and sends a response.
-Flight::halt([$code], [$message]) // Stop the framework with an optional status code and message.
-Flight::route($pattern, $callback) // Maps a URL pattern to a callback.
-Flight::redirect($url, [$code]) // Redirects to another URL.
-Flight::render($file, [$data], [$key]) // Renders a template file.
-Flight::error($exception) // Sends an HTTP 500 response.
+Flight::halt(int $code = 200, string $message = '') // Stop the framework with an optional status code and message.
+Flight::route(string $pattern, callable $callback, bool $pass_route = false) // Maps a URL pattern to a callback.
+Flight::redirect(string $url, int $code) // Redirects to another URL.
+Flight::render(string $file, array $data, ?string $key = null) // Renders a template file.
+Flight::error(Throwable $exception) // Sends an HTTP 500 response.
 Flight::notFound() // Sends an HTTP 404 response.
-Flight::etag($id, [$type]) // Performs ETag HTTP caching.
-Flight::lastModified($time) // Performs last modified HTTP caching.
-Flight::json($data, [$code], [$encode], [$charset], [$option]) // Sends a JSON response.
-Flight::jsonp($data, [$param], [$code], [$encode], [$charset], [$option]) // Sends a JSONP response.
+Flight::etag(string $id, string $type = 'string') // Performs ETag HTTP caching.
+Flight::lastModified(int $time) // Performs last modified HTTP caching.
+Flight::json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sends a JSON response.
+Flight::jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sends a JSONP response.
 ```
 
 Any custom methods added with `map` and `register` can also be filtered.
@@ -905,12 +928,10 @@ as an object instance.
 ```php
 require 'flight/autoload.php';
 
-use flight\Engine;
+$app = new flight\Engine();
 
-$app = new Engine();
-
-$app->route('/', function(){
-    echo 'hello world!';
+$app->route('/', function () {
+  echo 'hello world!';
 });
 
 $app->start();
diff --git a/VERSION b/VERSION
index 38f77a6..4a36342 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.0.1
+3.0.0
diff --git a/composer.json b/composer.json
index 8e2611b..2e0459c 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
 {
-    "name": "mikecao/flight",
-    "description": "Flight is a fast, simple, extensible framework for PHP. Flight enables you to quickly and easily build RESTful web applications.",
+    "name": "flightphp/core",
+    "description": "Flight is a fast, simple, extensible framework for PHP. Flight enables you to quickly and easily build RESTful web applications. This is the maintained fork of mikecao/flight",
     "homepage": "http://flightphp.com",
     "license": "MIT",
     "authors": [
@@ -9,16 +9,52 @@
             "email": "mike@mikecao.com",
             "homepage": "http://www.mikecao.com/",
             "role": "Original Developer"
+        },
+        {
+            "name": "Franyer Sánchez",
+            "email": "franyeradriansanchez@gmail.com",
+            "homepage": "https://faslatam.000webhostapp.com",
+            "role": "Maintainer"
+        },
+        {
+            "name": "n0nag0n",
+            "email": "n0nag0n@sky-9.com",
+            "role": "Maintainer"
         }
     ],
     "require": {
-        "php": "^7.4|^8.0|^8.1",
+        "php": "^7.4|^8.0|^8.1|^8.2",
         "ext-json": "*"
     },
     "autoload": {
-        "files": [ "flight/autoload.php", "flight/Flight.php" ]
+        "files": [
+            "flight/autoload.php",
+            "flight/Flight.php"
+        ]
     },
     "require-dev": {
-        "phpunit/phpunit": "^9.5"
-    }
+        "phpunit/phpunit": "^9.5",
+        "phpstan/phpstan": "^1.10",
+        "phpstan/extension-installer": "^1.3"
+    },
+    "config": {
+        "allow-plugins": {
+            "phpstan/extension-installer": true
+        }
+    },
+    "scripts": {
+        "test": "phpunit",
+		"test-coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage",
+        "lint": "phpstan --no-progress -cphpstan.neon"
+    },
+    "suggest": {
+        "phpstan/phpstan": "PHP Static Analysis Tool",
+		"latte/latte": "Latte template engine"
+    },
+	"suggest-dev": {
+		"tracy/tracy": "Tracy debugger"
+	},
+	"replace": {
+		"mikecao/flight": "2.0.2"
+	}
 }
diff --git a/flight.sublime-project b/flight.sublime-project
new file mode 100644
index 0000000..26552b2
--- /dev/null
+++ b/flight.sublime-project
@@ -0,0 +1,21 @@
+{
+    "folders": [
+        {
+            "path": ".",
+        }
+    ],
+    "settings": {
+        "LSP": {
+            "LSP-intelephense": {
+                "settings": {
+                    "intelephense.environment.phpVersion": "7.4.0",
+                    "intelephense.format.braces": "psr12",
+                },
+            },
+            "formatters":
+            {
+				"embedding.php": "LSP-intelephense"
+			},
+        },
+    },
+}
diff --git a/flight/Engine.php b/flight/Engine.php
index f8c996b..4b1bdca 100644
--- a/flight/Engine.php
+++ b/flight/Engine.php
@@ -30,7 +30,14 @@ use Throwable;
  * @method void start() Starts engine
  * @method void stop() Stops framework and outputs current response
  * @method void halt(int $code = 200, string $message = '') Stops processing and returns a given response.
+ * 
+ * Routing
  * @method void route(string $pattern, callable $callback, bool $pass_route = false) Routes a URL to a callback function.
+ * @method void get(string $pattern, callable $callback, bool $pass_route = false) Routes a GET URL to a callback function.
+ * @method void post(string $pattern, callable $callback, bool $pass_route = false) Routes a POST URL to a callback function.
+ * @method void put(string $pattern, callable $callback, bool $pass_route = false) Routes a PUT URL to a callback function.
+ * @method void patch(string $pattern, callable $callback, bool $pass_route = false) Routes a PATCH URL to a callback function.
+ * @method void delete(string $pattern, callable $callback, bool $pass_route = false) Routes a DELETE URL to a callback function.
  * @method Router router() Gets router
  *
  * Views
@@ -40,7 +47,7 @@ use Throwable;
  * Request-response
  * @method Request request() Gets current request
  * @method Response response() Gets current response
- * @method void error(Exception $e) Sends an HTTP 500 response for any errors.
+ * @method void error(Throwable $e) Sends an HTTP 500 response for any errors.
  * @method void notFound() Sends an HTTP 404 response when a URL is not found.
  * @method void redirect(string $url, int $code = 303)  Redirects the current request to another URL.
  * @method void json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) Sends a JSON response.
@@ -54,6 +61,7 @@ class Engine
 {
     /**
      * Stored variables.
+     * @var array
      */
     protected array $vars;
 
@@ -67,6 +75,13 @@ class Engine
      */
     protected Dispatcher $dispatcher;
 
+	/**
+	 * If the framework has been initialized or not
+	 *
+	 * @var boolean
+	 */
+	protected bool $initialized = false;
+
     /**
      * Constructor.
      */
@@ -84,7 +99,7 @@ class Engine
      * Handles calls to class methods.
      *
      * @param string $name   Method name
-     * @param array  $params Method parameters
+     * @param array  $params Method parameters
      *
      * @throws Exception
      *
@@ -114,7 +129,7 @@ class Engine
      */
     public function init(): void
     {
-        static $initialized = false;
+        $initialized = $this->initialized;
         $self = $this;
 
         if ($initialized) {
@@ -155,8 +170,8 @@ class Engine
         $this->before('start', function () use ($self) {
             // Enable error handling
             if ($self->get('flight.handle_errors')) {
-                set_error_handler([$self, 'handleError']);
-                set_exception_handler([$self, 'handleException']);
+                set_error_handler(array($self, 'handleError'));
+                set_exception_handler(array($self, 'handleException'));
             }
 
             // Set case-sensitivity
@@ -165,7 +180,7 @@ class Engine
             $self->response()->content_length = $self->get('flight.content_length');
         });
 
-        $initialized = true;
+        $this->initialized = true;
     }
 
     /**
@@ -177,23 +192,26 @@ class Engine
      * @param int    $errline Error file line number
      *
      * @throws ErrorException
+     * @return bool
      */
     public function handleError(int $errno, string $errstr, string $errfile, int $errline)
     {
         if ($errno & error_reporting()) {
             throw new ErrorException($errstr, $errno, 0, $errfile, $errline);
         }
+
+        return false;
     }
 
     /**
      * Custom exception handler. Logs exceptions.
      *
-     * @param Exception $e Thrown exception
+     * @param Throwable $e Thrown exception
      */
     public function handleException($e): void
     {
         if ($this->get('flight.log_errors')) {
-            error_log($e->getMessage());
+            error_log($e->getMessage()); // @codeCoverageIgnore
         }
 
         $this->error($e);
@@ -203,7 +221,7 @@ class Engine
      * Maps a callback to a framework method.
      *
      * @param string   $name     Method name
-     * @param callback $callback Callback function
+     * @param callable $callback Callback function
      *
      * @throws Exception If trying to map over a framework method
      */
@@ -218,11 +236,12 @@ class Engine
 
     /**
      * Registers a class to a framework method.
+     * @template T of object
      *
      * @param string        $name     Method name
-     * @param string        $class    Class name
-     * @param array         $params   Class initialization parameters
-     * @param callable|null $callback $callback Function to call after object instantiation
+     * @param class-string $class  Class name
+     * @param array $params   Class initialization parameters
+     * @param ?callable(T $instance): void $callback Function to call after object instantiation
      *
      * @throws Exception If trying to map over a framework method
      */
@@ -239,7 +258,7 @@ class Engine
      * Adds a pre-filter to a method.
      *
      * @param string   $name     Method name
-     * @param callback $callback Callback function
+     * @param callable $callback Callback function
      */
     public function before(string $name, callable $callback): void
     {
@@ -250,7 +269,7 @@ class Engine
      * Adds a post-filter to a method.
      *
      * @param string   $name     Method name
-     * @param callback $callback Callback function
+     * @param callable $callback Callback function
      */
     public function after(string $name, callable $callback): void
     {
@@ -348,7 +367,7 @@ class Engine
 
         // Flush any existing output
         if (ob_get_length() > 0) {
-            $response->write(ob_get_clean());
+            $response->write(ob_get_clean()); // @codeCoverageIgnore
         }
 
         // Enable output buffering
@@ -392,9 +411,10 @@ class Engine
      */
     public function _error($e): void
     {
-        $msg = sprintf('500 Internal Server Error
' .
-            '%s (%s)
' .
-            '%s
',
+        $msg = sprintf(
+            '500 Internal Server Error
' .
+                '%s (%s)
' .
+                '%s
',
             $e->getMessage(),
             $e->getCode(),
             $e->getTraceAsString()
@@ -406,9 +426,11 @@ class Engine
                 ->status(500)
                 ->write($msg)
                 ->send();
+		// @codeCoverageIgnoreStart
         } catch (Throwable $t) {
             exit($msg);
         }
+		// @codeCoverageIgnoreEnd
     }
 
     /**
@@ -427,7 +449,8 @@ class Engine
                 $response->status($code);
             }
 
-            $response->write(ob_get_clean());
+            $content = ob_get_clean();
+            $response->write($content ?: '');
 
             $response->send();
         }
@@ -437,7 +460,7 @@ class Engine
      * Routes a URL to a callback function.
      *
      * @param string   $pattern    URL pattern to match
-     * @param callback $callback   Callback function
+     * @param callable $callback   Callback function
      * @param bool     $pass_route Pass the matching route object to the callback
      */
     public function _route(string $pattern, callable $callback, bool $pass_route = false): void
@@ -449,7 +472,7 @@ class Engine
      * Routes a URL to a callback function.
      *
      * @param string   $pattern    URL pattern to match
-     * @param callback $callback   Callback function
+     * @param callable $callback   Callback function
      * @param bool     $pass_route Pass the matching route object to the callback
      */
     public function _post(string $pattern, callable $callback, bool $pass_route = false): void
@@ -461,7 +484,7 @@ class Engine
      * Routes a URL to a callback function.
      *
      * @param string   $pattern    URL pattern to match
-     * @param callback $callback   Callback function
+     * @param callable $callback   Callback function
      * @param bool     $pass_route Pass the matching route object to the callback
      */
     public function _put(string $pattern, callable $callback, bool $pass_route = false): void
@@ -473,7 +496,7 @@ class Engine
      * Routes a URL to a callback function.
      *
      * @param string   $pattern    URL pattern to match
-     * @param callback $callback   Callback function
+     * @param callable $callback   Callback function
      * @param bool     $pass_route Pass the matching route object to the callback
      */
     public function _patch(string $pattern, callable $callback, bool $pass_route = false): void
@@ -485,7 +508,7 @@ class Engine
      * Routes a URL to a callback function.
      *
      * @param string   $pattern    URL pattern to match
-     * @param callback $callback   Callback function
+     * @param callable $callback   Callback function
      * @param bool     $pass_route Pass the matching route object to the callback
      */
     public function _delete(string $pattern, callable $callback, bool $pass_route = false): void
@@ -498,6 +521,7 @@ class Engine
      *
      * @param int    $code    HTTP status code
      * @param string $message Response message
+	 * 
      */
     public function _halt(int $code = 200, string $message = ''): void
     {
@@ -506,7 +530,10 @@ class Engine
             ->status($code)
             ->write($message)
             ->send();
-        exit();
+		// apologies for the crappy hack here...
+		if($message !== 'skip---exit') {
+			exit(); // @codeCoverageIgnore
+		}
     }
 
     /**
@@ -519,8 +546,8 @@ class Engine
             ->status(404)
             ->write(
                 '404 Not Found
' .
-                'The page you have requested could not be found.
' .
-                str_repeat(' ', 512)
+                    'The page you have requested could not be found.
' .
+                    str_repeat(' ', 512)
             )
             ->send();
     }
@@ -555,7 +582,7 @@ class Engine
      * Renders a template.
      *
      * @param string      $file Template file
-     * @param array|null  $data Template data
+     * @param ?array $data Template data
      * @param string|null $key  View variable name
      *
      * @throws Exception
@@ -639,8 +666,10 @@ class Engine
 
         $this->response()->header('ETag', $id);
 
-        if (isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
-            $_SERVER['HTTP_IF_NONE_MATCH'] === $id) {
+        if (
+            isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
+            $_SERVER['HTTP_IF_NONE_MATCH'] === $id
+        ) {
             $this->halt(304);
         }
     }
@@ -654,8 +683,10 @@ class Engine
     {
         $this->response()->header('Last-Modified', gmdate('D, d M Y H:i:s \G\M\T', $time));
 
-        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
-            strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time) {
+        if (
+            isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
+            strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time
+        ) {
             $this->halt(304);
         }
     }
diff --git a/flight/Flight.php b/flight/Flight.php
index 101c455..69a8b65 100644
--- a/flight/Flight.php
+++ b/flight/Flight.php
@@ -18,36 +18,27 @@ use flight\template\View;
 /**
  * The Flight class is a static representation of the framework.
  *
- * Core.
- *
  * @method  static void start() Starts the framework.
- * @method  static void path($path) Adds a path for autoloading classes.
+ * @method  static void path(string $path) Adds a path for autoloading classes.
  * @method  static void stop() Stops the framework and sends a response.
- * @method  static void halt($code = 200, $message = '') Stop the framework with an optional status code and message.
+ * @method  static void halt(int $code = 200, string $message = '') Stop the framework with an optional status code and message.
  *
- * Routing.
- * @method  static void route($pattern, $callback) Maps a URL pattern to a callback.
+ * @method  static void route(string $pattern, callable $callback, bool $pass_route = false) Maps a URL pattern to a callback.
  * @method  static Router router() Returns Router instance.
  *
- * Extending & Overriding.
- * @method  static void map($name, $callback) Creates a custom framework method.
- * @method  static void register($name, $class, array $params = array(), $callback = null) Registers a class to a framework method.
+ * @method  static void map(string $name, callable $callback) Creates a custom framework method.
  *
- * Filtering.
  * @method  static void before($name, $callback) Adds a filter before a framework method.
  * @method  static void after($name, $callback) Adds a filter after a framework method.
  *
- * Variables.
  * @method  static void set($key, $value) Sets a variable.
  * @method  static mixed get($key) Gets a variable.
  * @method  static bool has($key) Checks if a variable is set.
  * @method  static void clear($key = null) Clears a variable.
  *
- * Views.
  * @method  static void render($file, array $data = null, $key = null) Renders a template file.
  * @method  static View view() Returns View instance.
  *
- * Request & Response.
  * @method  static Request request() Returns Request instance.
  * @method  static Response response() Returns Response instance.
  * @method  static void redirect($url, $code = 303) Redirects to another URL.
@@ -56,7 +47,6 @@ use flight\template\View;
  * @method  static void error($exception) Sends an HTTP 500 response.
  * @method  static void notFound() Sends an HTTP 404 response.
  *
- * HTTP Caching.
  * @method  static void etag($id, $type = 'strong') Performs ETag HTTP caching.
  * @method  static void lastModified($time) Performs last modified HTTP caching.
  */
@@ -67,24 +57,50 @@ class Flight
      */
     private static Engine $engine;
 
-    // Don't allow object instantiation
+	/**
+	 * Don't allow object instantiation
+	 * 
+	 * @codeCoverageIgnore
+	 * @return void
+	 */
     private function __construct()
     {
     }
 
-    private function __destruct()
+	/**
+	 * Forbid cloning the class
+	 *
+	 * @codeCoverageIgnore
+	 * @return void
+	 */
+    private function __clone()
     {
     }
 
-    private function __clone()
+    /**
+     * Registers a class to a framework method.
+     * @template T of object
+     * @param  string $name Static method name
+     * ```
+     * Flight::register('user', User::class);
+     *
+     * Flight::user(); # <- Return a User instance
+     * ```
+     * @param  class-string $class Fully Qualified Class Name
+     * @param  array  $params   Class constructor params
+     * @param  ?Closure(T $instance): void $callback Perform actions with the instance
+     * @return void
+     */
+    static function register($name, $class, $params = array(), $callback = null)
     {
+        static::__callStatic('register', func_get_args());
     }
 
     /**
      * Handles calls to static methods.
      *
      * @param string $name   Method name
-     * @param array  $params Method parameters
+     * @param array  $params Method parameters
      *
      * @throws Exception
      *
diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php
index 50b73f0..4ce6854 100644
--- a/flight/core/Dispatcher.php
+++ b/flight/core/Dispatcher.php
@@ -23,11 +23,13 @@ class Dispatcher
 {
     /**
      * Mapped events.
+     * @var array
      */
     protected array $events = [];
 
     /**
      * Method filters.
+     * @var array>>
      */
     protected array $filters = [];
 
@@ -35,9 +37,9 @@ class Dispatcher
      * Dispatches an event.
      *
      * @param string $name   Event name
-     * @param array  $params Callback parameters
+     * @param array  $params Callback parameters
      *
-     *@throws Exception
+     * @throws Exception
      *
      * @return mixed|null Output of callback
      */
@@ -65,7 +67,7 @@ class Dispatcher
      * Assigns a callback to an event.
      *
      * @param string   $name     Event name
-     * @param callback $callback Callback function
+     * @param callable $callback Callback function
      */
     final public function set(string $name, callable $callback): void
     {
@@ -77,7 +79,7 @@ class Dispatcher
      *
      * @param string $name Event name
      *
-     * @return callback $callback Callback function
+     * @return callable $callback Callback function
      */
     final public function get(string $name): ?callable
     {
@@ -118,7 +120,7 @@ class Dispatcher
      *
      * @param string   $name     Event name
      * @param string   $type     Filter type
-     * @param callback $callback Callback function
+     * @param callable $callback Callback function
      */
     final public function hook(string $name, string $type, callable $callback): void
     {
@@ -128,8 +130,8 @@ class Dispatcher
     /**
      * Executes a chain of method filters.
      *
-     * @param array $filters Chain of filters
-     * @param array $params  Method parameters
+     * @param array $filters Chain of filters
+     * @param array $params  Method parameters
      * @param mixed $output  Method output
      *
      * @throws Exception
@@ -148,10 +150,10 @@ class Dispatcher
     /**
      * Executes a callback function.
      *
-     * @param array|callback $callback Callback function
-     * @param array          $params   Function parameters
+     * @param callable|array $callback Callback function
+     * @param array          $params   Function parameters
      *
-     *@throws Exception
+     * @throws Exception
      *
      * @return mixed Function results
      */
@@ -170,7 +172,7 @@ class Dispatcher
      * Calls a function.
      *
      * @param callable|string $func   Name of function to call
-     * @param array           $params Function parameters
+     * @param array           $params Function parameters
      *
      * @return mixed Function results
      */
@@ -203,7 +205,7 @@ class Dispatcher
      * Invokes a method.
      *
      * @param mixed $func   Class method
-     * @param array $params Class method parameters
+     * @param array $params Class method parameters
      *
      * @return mixed Function results
      */
@@ -230,6 +232,8 @@ class Dispatcher
                 return ($instance) ?
                     $class->$method($params[0], $params[1], $params[2]) :
                     $class::$method($params[0], $params[1], $params[2]);
+			// This will be refactored soon enough
+			// @codeCoverageIgnoreStart
             case 4:
                 return ($instance) ?
                     $class->$method($params[0], $params[1], $params[2], $params[3]) :
@@ -240,6 +244,7 @@ class Dispatcher
                     $class::$method($params[0], $params[1], $params[2], $params[3], $params[4]);
             default:
                 return \call_user_func_array($func, $params);
+			// @codeCoverageIgnoreEnd
         }
     }
 
diff --git a/flight/core/Loader.php b/flight/core/Loader.php
index 95874a6..c438445 100644
--- a/flight/core/Loader.php
+++ b/flight/core/Loader.php
@@ -10,6 +10,7 @@ declare(strict_types=1);
 
 namespace flight\core;
 
+use Closure;
 use Exception;
 use ReflectionClass;
 use ReflectionException;
@@ -24,26 +25,30 @@ class Loader
 {
     /**
      * Registered classes.
+     * @var array, ?callable}> $classes
      */
     protected array $classes = [];
 
     /**
      * Class instances.
+     * @var array
      */
     protected array $instances = [];
 
     /**
      * Autoload directories.
+     * @var array
      */
     protected static array $dirs = [];
 
     /**
      * Registers a class.
+     * @template T of object
      *
      * @param string          $name     Registry name
-     * @param callable|string $class    Class name or function to instantiate class
-     * @param array           $params   Class initialization parameters
-     * @param callable|null   $callback $callback Function to call after object instantiation
+     * @param class-string $class    Class name or function to instantiate class
+     * @param array           $params   Class initialization parameters
+     * @param ?callable(T $instance): void   $callback $callback Function to call after object instantiation
      */
     public function register(string $name, $class, array $params = [], ?callable $callback = null): void
     {
@@ -77,7 +82,7 @@ class Loader
         $obj = null;
 
         if (isset($this->classes[$name])) {
-            [$class, $params, $callback] = $this->classes[$name];
+            [0 => $class, 1 => $params, 2 => $callback] = $this->classes[$name];
 
             $exists = isset($this->instances[$name]);
 
@@ -116,15 +121,16 @@ class Loader
 
     /**
      * Gets a new instance of a class.
+     * @template T of object
      *
-     * @param callable|string $class  Class name or callback function to instantiate class
-     * @param array           $params Class initialization parameters
+     * @param class-string|Closure(): class-string $class  Class name or callback function to instantiate class
+     * @param array           $params Class initialization parameters
      *
      * @throws Exception
      *
-     * @return object Class instance
+     * @return T Class instance
      */
-    public function newInstance($class, array $params = []): object
+    public function newInstance($class, array $params = [])
     {
         if (\is_callable($class)) {
             return \call_user_func_array($class, $params);
@@ -135,6 +141,7 @@ class Loader
                 return new $class();
             case 1:
                 return new $class($params[0]);
+			// @codeCoverageIgnoreStart
             case 2:
                 return new $class($params[0], $params[1]);
             case 3:
@@ -143,6 +150,7 @@ class Loader
                 return new $class($params[0], $params[1], $params[2], $params[3]);
             case 5:
                 return new $class($params[0], $params[1], $params[2], $params[3], $params[4]);
+			// @codeCoverageIgnoreEnd
             default:
                 try {
                     $refClass = new ReflectionClass($class);
@@ -179,14 +187,14 @@ class Loader
      * Starts/stops autoloader.
      *
      * @param bool  $enabled Enable/disable autoloading
-     * @param mixed $dirs    Autoload directories
+     * @param string|iterable $dirs    Autoload directories
      */
     public static function autoload(bool $enabled = true, $dirs = []): void
     {
         if ($enabled) {
             spl_autoload_register([__CLASS__, 'loadClass']);
         } else {
-            spl_autoload_unregister([__CLASS__, 'loadClass']);
+            spl_autoload_unregister([__CLASS__, 'loadClass']); // @codeCoverageIgnore
         }
 
         if (!empty($dirs)) {
@@ -216,7 +224,7 @@ class Loader
     /**
      * Adds a directory for autoloading classes.
      *
-     * @param mixed $dir Directory path
+     * @param string|iterable $dir Directory path
      */
     public static function addDirectory($dir): void
     {
diff --git a/flight/net/Request.php b/flight/net/Request.php
index 75d4665..8a40a50 100644
--- a/flight/net/Request.php
+++ b/flight/net/Request.php
@@ -18,23 +18,24 @@ use flight\util\Collection;
  * are stored and accessible via the Request object.
  *
  * The default request properties are:
- *   url - The URL being requested
- *   base - The parent subdirectory of the URL
- *   method - The request method (GET, POST, PUT, DELETE)
- *   referrer - The referrer URL
- *   ip - IP address of the client
- *   ajax - Whether the request is an AJAX request
- *   scheme - The server protocol (http, https)
- *   user_agent - Browser information
- *   type - The content type
- *   length - The content length
- *   query - Query string parameters
- *   data - Post parameters
- *   cookies - Cookie parameters
- *   files - Uploaded files
- *   secure - Connection is secure
- *   accept - HTTP accept parameters
- *   proxy_ip - Proxy IP address of the client
+ *
+ *   - **url** - The URL being requested
+ *   - **base** - The parent subdirectory of the URL
+ *   - **method** - The request method (GET, POST, PUT, DELETE)
+ *   - **referrer** - The referrer URL
+ *   - **ip** - IP address of the client
+ *   - **ajax** - Whether the request is an AJAX request
+ *   - **scheme** - The server protocol (http, https)
+ *   - **user_agent** - Browser information
+ *   - **type** - The content type
+ *   - **length** - The content length
+ *   - **query** - Query string parameters
+ *   - **data** - Post parameters
+ *   - **cookies** - Cookie parameters
+ *   - **files** - Uploaded files
+ *   - **secure** - Connection is secure
+ *   - **accept** - HTTP accept parameters
+ *   - **proxy_ip** - Proxy IP address of the client
  */
 final class Request
 {
@@ -128,12 +129,25 @@ final class Request
      */
     public string $host;
 
+    /**
+     * Stream path for where to pull the request body from
+     *
+     * @var string
+     */
+    private string $stream_path = 'php://input';
+
+    /**
+     * @var string Raw HTTP request body
+     */
+    public string $body = '';
+
     /**
      * Constructor.
      *
-     * @param array $config Request configuration
+     * @param array $config Request configuration
+     * @param string
      */
-    public function __construct(array $config = [])
+    public function __construct($config = array())
     {
         // Default properties
         if (empty($config)) {
@@ -165,7 +179,8 @@ final class Request
     /**
      * Initialize request properties.
      *
-     * @param array $properties Array of request properties
+     * @param array $properties Array of request properties
+     * @return static
      */
     public function init(array $properties = [])
     {
@@ -175,6 +190,9 @@ final class Request
         }
 
         // Get the requested URL without the base directory
+		// This rewrites the url in case the public url and base directories match 
+		// (such as installing on a subdirectory in a web server)
+		// @see testInitUrlSameAsBaseDirectory
         if ('/' !== $this->base && '' !== $this->base && 0 === strpos($this->url, $this->base)) {
             $this->url = substr($this->url, \strlen($this->base));
         }
@@ -191,7 +209,7 @@ final class Request
 
         // Check for JSON input
         if (0 === strpos($this->type, 'application/json')) {
-            $body = self::getBody();
+            $body = $this->getBody();
             if ('' !== $body && null !== $body) {
                 $data = json_decode($body, true);
                 if (is_array($data)) {
@@ -199,6 +217,8 @@ final class Request
                 }
             }
         }
+
+        return $this;
     }
 
     /**
@@ -206,20 +226,22 @@ final class Request
      *
      * @return string Raw HTTP request body
      */
-    public static function getBody(): ?string
+    public function getBody(): ?string
     {
-        static $body;
+        $body = $this->body;
 
-        if (null !== $body) {
+        if ('' !== $body) {
             return $body;
         }
 
         $method = self::getMethod();
 
         if ('POST' === $method || 'PUT' === $method || 'DELETE' === $method || 'PATCH' === $method) {
-            $body = file_get_contents('php://input');
+            $body = file_get_contents($this->stream_path);
         }
 
+        $this->body = $body;
+
         return $body;
     }
 
@@ -287,11 +309,11 @@ final class Request
      *
      * @param string $url URL string
      *
-     * @return array Query parameters
+     * @return array>
      */
     public static function parseQuery(string $url): array
     {
-        $params = [];
+        $params = array();
 
         $args = parse_url($url);
         if (isset($args['query'])) {
diff --git a/flight/net/Response.php b/flight/net/Response.php
index e21fc9a..38d06ec 100644
--- a/flight/net/Response.php
+++ b/flight/net/Response.php
@@ -25,7 +25,7 @@ class Response
     public bool $content_length = true;
 
     /**
-     * @var array HTTP status codes
+     * @var array HTTP status codes
      */
     public static array $codes = [
         100 => 'Continue',
@@ -103,7 +103,7 @@ class Response
     protected int $status = 200;
 
     /**
-     * @var array HTTP headers
+     * @var array> HTTP headers
      */
     protected array $headers = [];
 
@@ -124,7 +124,7 @@ class Response
      *
      * @throws Exception If invalid status code
      *
-     * @return int|object Self reference
+     * @return int|static Self reference
      */
     public function status(?int $code = null)
     {
@@ -144,10 +144,10 @@ class Response
     /**
      * Adds a header to the response.
      *
-     * @param array|string $name  Header name or array of names and values
+     * @param array|string $name  Header name or array of names and values
      * @param string|null  $value Header value
      *
-     * @return object Self reference
+     * @return static Self reference
      */
     public function header($name, ?string $value = null)
     {
@@ -164,8 +164,7 @@ class Response
 
     /**
      * Returns the headers from the response.
-     *
-     * @return array
+     * @return array>
      */
     public function headers()
     {
@@ -203,7 +202,7 @@ class Response
     /**
      * Sets caching headers for the response.
      *
-     * @param int|string $expires Expiration time
+     * @param int|string|false $expires Expiration time as time() or as strtotime() string value
      *
      * @return Response Self reference
      */
@@ -238,7 +237,8 @@ class Response
     {
         // Send status code header
         if (false !== strpos(\PHP_SAPI, 'cgi')) {
-            header(
+			// @codeCoverageIgnoreStart
+            $this->setRealHeader(
                 sprintf(
                     'Status: %d %s',
                     $this->status,
@@ -246,13 +246,15 @@ class Response
                 ),
                 true
             );
+			// @codeCoverageIgnoreEnd
         } else {
-            header(
+            $this->setRealHeader(
                 sprintf(
                     '%s %d %s',
                     $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1',
                     $this->status,
-                    self::$codes[$this->status]),
+                    self::$codes[$this->status]
+                ),
                 true,
                 $this->status
             );
@@ -262,10 +264,10 @@ class Response
         foreach ($this->headers as $field => $value) {
             if (\is_array($value)) {
                 foreach ($value as $v) {
-                    header($field . ': ' . $v, false);
+                    $this->setRealHeader($field . ': ' . $v, false);
                 }
             } else {
-                header($field . ': ' . $value);
+                $this->setRealHeader($field . ': ' . $value);
             }
         }
 
@@ -274,13 +276,27 @@ class Response
             $length = $this->getContentLength();
 
             if ($length > 0) {
-                header('Content-Length: ' . $length);
+                $this->setRealHeader('Content-Length: ' . $length);
             }
         }
 
         return $this;
     }
 
+	/**
+	 * Sets a real header. Mostly used for test mocking.
+	 *
+	 * @param string $header_string The header string you would pass to header()
+	 * @param bool $replace The optional replace parameter indicates whether the header should replace a previous similar header, or add a second header of the same type. By default it will replace, but if you pass in false as the second argument you can force multiple headers of the same type.
+	 * @param int $response_code The response code to send
+	 * @return self
+	 * @codeCoverageIgnore
+	 */
+	public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): self {
+		header($header_string, $replace, $response_code);
+		return $this;
+	}
+
     /**
      * Gets the content length.
      *
@@ -294,7 +310,7 @@ class Response
     }
 
     /**
-     * Gets whether response was sent.
+     * Gets whether response body was sent.
      */
     public function sent(): bool
     {
@@ -307,11 +323,11 @@ class Response
     public function send(): void
     {
         if (ob_get_length() > 0) {
-            ob_end_clean();
+            ob_end_clean(); // @codeCoverageIgnore
         }
 
         if (!headers_sent()) {
-            $this->sendHeaders();
+            $this->sendHeaders(); // @codeCoverageIgnore
         }
 
         echo $this->body;
diff --git a/flight/net/Route.php b/flight/net/Route.php
index 91a6aff..fbfc20b 100644
--- a/flight/net/Route.php
+++ b/flight/net/Route.php
@@ -28,12 +28,12 @@ final class Route
     public $callback;
 
     /**
-     * @var array HTTP methods
+     * @var array HTTP methods
      */
     public array $methods = [];
 
     /**
-     * @var array Route parameters
+     * @var array Route parameters
      */
     public array $params = [];
 
@@ -56,8 +56,8 @@ final class Route
      * Constructor.
      *
      * @param string $pattern  URL pattern
-     * @param mixed  $callback Callback function
-     * @param array  $methods  HTTP methods
+     * @param callable  $callback Callback function
+     * @param array  $methods  HTTP methods
      * @param bool   $pass     Pass self in callback parameters
      */
     public function __construct(string $pattern, $callback, array $methods, bool $pass)
diff --git a/flight/net/Router.php b/flight/net/Router.php
index dcf1591..f1f8842 100644
--- a/flight/net/Router.php
+++ b/flight/net/Router.php
@@ -23,6 +23,7 @@ class Router
     public bool $case_sensitive = false;
     /**
      * Mapped routes.
+     * @var array
      */
     protected array $routes = [];
 
@@ -34,7 +35,7 @@ class Router
     /**
      * Gets mapped routes.
      *
-     * @return array Array of routes
+     * @return array Array of routes
      */
     public function getRoutes(): array
     {
@@ -53,7 +54,7 @@ class Router
      * Maps a URL pattern to a callback function.
      *
      * @param string   $pattern    URL pattern to match
-     * @param callback $callback   Callback function
+     * @param callable $callback   Callback function
      * @param bool     $pass_route Pass the matching route object to the callback
      */
     public function map(string $pattern, callable $callback, bool $pass_route = false): void
@@ -81,7 +82,7 @@ class Router
     {
         $url_decoded = urldecode($request->url);
         while ($route = $this->current()) {
-            if (false !== $route && $route->matchMethod($request->method) && $route->matchUrl($url_decoded, $this->case_sensitive)) {
+            if ($route->matchMethod($request->method) && $route->matchUrl($url_decoded, $this->case_sensitive)) {
                 return $route;
             }
             $this->next();
diff --git a/flight/template/View.php b/flight/template/View.php
index 90dbb81..818a100 100644
--- a/flight/template/View.php
+++ b/flight/template/View.php
@@ -34,7 +34,7 @@ class View
     /**
      * View variables.
      *
-     * @var array
+     * @var array
      */
     protected $vars = [];
 
@@ -70,8 +70,9 @@ class View
     /**
      * Sets a template variable.
      *
-     * @param mixed  $key   Key
-     * @param string $value Value
+     * @param string|iterable  $key   Key
+     * @param mixed $value Value
+     * @return static
      */
     public function set($key, $value = null)
     {
@@ -82,6 +83,8 @@ class View
         } else {
             $this->vars[$key] = $value;
         }
+
+        return $this;
     }
 
     /**
@@ -100,6 +103,7 @@ class View
      * Unsets a template variable. If no key is passed in, clear all variables.
      *
      * @param string $key Key
+     * @return static
      */
     public function clear($key = null)
     {
@@ -108,15 +112,18 @@ class View
         } else {
             unset($this->vars[$key]);
         }
+
+        return $this;
     }
 
     /**
      * Renders a template.
      *
      * @param string $file Template file
-     * @param array  $data Template data
+     * @param array  $data Template data
      *
      * @throws \Exception If template not found
+     * @return void
      */
     public function render($file, $data = null)
     {
@@ -139,7 +146,7 @@ class View
      * Gets the output of a template.
      *
      * @param string $file Template file
-     * @param array  $data Template data
+     * @param array  $data Template data
      *
      * @return string Output of template
      */
@@ -179,11 +186,13 @@ class View
             $file .= $ext;
         }
 
-        if (('/' == substr($file, 0, 1))) {
+		$is_windows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
+
+        if (('/' == substr($file, 0, 1)) || ($is_windows === true && ':' == substr($file, 1, 1))) {
             return $file;
         }
 
-        return $this->path . '/' . $file;
+        return $this->path . DIRECTORY_SEPARATOR . $file;
     }
 
     /**
@@ -195,6 +204,8 @@ class View
      */
     public function e($str)
     {
-        echo htmlentities($str);
+		$value = htmlentities($str);
+        echo $value;
+        return $value;
     }
 }
diff --git a/flight/util/Collection.php b/flight/util/Collection.php
index 97658ec..01ddb81 100644
--- a/flight/util/Collection.php
+++ b/flight/util/Collection.php
@@ -11,30 +11,32 @@ declare(strict_types=1);
 namespace flight\util;
 
 use ArrayAccess;
-use function count;
 use Countable;
 use Iterator;
 use JsonSerializable;
 
 if (!interface_exists('JsonSerializable')) {
-    require_once __DIR__ . '/LegacyJsonSerializable.php';
+    require_once __DIR__ . '/LegacyJsonSerializable.php'; // @codeCoverageIgnore
 }
 
 /**
  * The Collection class allows you to access a set of data
  * using both array and object notation.
+ * @implements ArrayAccess
+ * @implements Iterator
  */
 final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable
 {
     /**
      * Collection data.
+     * @var array
      */
     private array $data;
 
     /**
      * Constructor.
      *
-     * @param array $data Initial data
+     * @param array $data Initial data
      */
     public function __construct(array $data = [])
     {
@@ -102,11 +104,11 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ
     /**
      * Sets an item at the offset.
      *
-     * @param string $offset Offset
+     * @param ?string $offset Offset
      * @param mixed  $value  Value
      */
     #[\ReturnTypeWillChange]
-    public function offsetSet($offset, $value)
+    public function offsetSet($offset, $value): void
     {
         if (null === $offset) {
             $this->data[] = $value;
@@ -169,13 +171,11 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ
 
     /**
      * Gets the next collection value.
-     *
-     * @return mixed Value
      */
     #[\ReturnTypeWillChange]
-    public function next()
+    public function next(): void
     {
-        return next($this->data);
+        next($this->data);
     }
 
     /**
@@ -187,7 +187,7 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ
     {
         $key = key($this->data);
 
-        return null !== $key && false !== $key;
+        return null !== $key;
     }
 
     /**
@@ -203,7 +203,7 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ
     /**
      * Gets the item keys.
      *
-     * @return array Collection keys
+     * @return array Collection keys
      */
     public function keys(): array
     {
@@ -213,7 +213,7 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ
     /**
      * Gets the collection data.
      *
-     * @return array Collection data
+     * @return array Collection data
      */
     public function getData(): array
     {
@@ -223,19 +223,15 @@ final class Collection implements ArrayAccess, Iterator, Countable, JsonSerializ
     /**
      * Sets the collection data.
      *
-     * @param array $data New collection data
+     * @param array $data New collection data
      */
     public function setData(array $data): void
     {
         $this->data = $data;
     }
 
-    /**
-     * Gets the collection data which can be serialized to JSON.
-     *
-     * @return array Collection data which can be serialized by json_encode
-     */
-    public function jsonSerialize(): array
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
     {
         return $this->data;
     }
diff --git a/flight/util/LegacyJsonSerializable.php b/flight/util/LegacyJsonSerializable.php
index 0c973aa..39e1721 100644
--- a/flight/util/LegacyJsonSerializable.php
+++ b/flight/util/LegacyJsonSerializable.php
@@ -9,5 +9,9 @@ declare(strict_types=1);
  */
 interface LegacyJsonSerializable
 {
+    /**
+     * Gets the collection data which can be serialized to JSON.
+     * @return mixed Collection data which can be serialized by json_encode
+     */
     public function jsonSerialize();
 }
diff --git a/flight/util/ReturnTypeWillChange.php b/flight/util/ReturnTypeWillChange.php
new file mode 100644
index 0000000..3ed841b
--- /dev/null
+++ b/flight/util/ReturnTypeWillChange.php
@@ -0,0 +1,3 @@
+
+
+  
+    
+      flight/
+    
+  
+  
+    
+      tests/
+    
+  
+  
+
diff --git a/tests/AutoloadTest.php b/tests/AutoloadTest.php
index 0fbb958..993659b 100644
--- a/tests/AutoloadTest.php
+++ b/tests/AutoloadTest.php
@@ -8,9 +8,6 @@
 
 use flight\Engine;
 
-require_once 'vendor/autoload.php';
-require_once __DIR__ . '/../flight/autoload.php';
-
 class AutoloadTest extends PHPUnit\Framework\TestCase
 {
     /**
diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php
new file mode 100644
index 0000000..8d6ba76
--- /dev/null
+++ b/tests/CollectionTest.php
@@ -0,0 +1,98 @@
+
+ * @license     MIT, http://flightphp.com/license
+ */
+class CollectionTest extends PHPUnit\Framework\TestCase
+{
+	/**
+	 * @var \flight\util\Collection
+	 */
+	private $collection;
+
+	protected function setUp(): void
+	{
+		$this->collection = new \flight\util\Collection(['a' => 1, 'b' => 2]);
+	}
+
+	// Get an item
+	public function testGet()
+	{
+		$this->assertEquals(1, $this->collection->a);
+	}
+
+	// Set an item
+	public function testSet()
+	{
+		$this->collection->c = 3;
+		$this->assertEquals(3, $this->collection->c);
+	}
+
+	// Check if an item exists
+	public function testExists()
+	{
+		$this->assertTrue(isset($this->collection->a));
+	}
+
+	// Unset an item
+	public function testUnset()
+	{
+		unset($this->collection->a);
+		$this->assertFalse(isset($this->collection->a));
+	}
+
+	// Count items
+	public function testCount()
+	{
+		$this->assertEquals(2, count($this->collection));
+	}
+
+	// Iterate through items
+	public function testIterate()
+	{
+		$items = [];
+		foreach ($this->collection as $key => $value) {
+			$items[$key] = $value;
+		}
+
+		$this->assertEquals(['a' => 1, 'b' => 2], $items);
+	}
+
+	public function testJsonSerialize()
+	{
+		$this->assertEquals(['a' => 1, 'b' => 2], $this->collection->jsonSerialize());
+	}
+
+	public function testOffsetSetWithNullOffset() {
+		$this->collection->offsetSet(null, 3);
+		$this->assertEquals(3, $this->collection->offsetGet(0));
+	}
+
+	public function testOffsetExists() {
+		$this->collection->a = 1;
+		$this->assertTrue($this->collection->offsetExists('a'));
+	}
+
+	public function testOffsetUnset() {
+		$this->collection->a = 1;
+		$this->assertTrue($this->collection->offsetExists('a'));
+		$this->collection->offsetUnset('a');
+		$this->assertFalse($this->collection->offsetExists('a'));
+	}
+
+	public function testKeys() {
+		$this->collection->a = 1;
+		$this->collection->b = 2;
+		$this->assertEquals(['a', 'b'], $this->collection->keys());
+	}
+
+	public function testClear() {
+		$this->collection->a = 1;
+		$this->collection->b = 2;
+		$this->assertEquals(['a', 'b'], $this->collection->keys());
+		$this->collection->clear();
+		$this->assertEquals(0, $this->collection->count());
+	}
+}
diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php
index 63421a4..0fc5d47 100644
--- a/tests/DispatcherTest.php
+++ b/tests/DispatcherTest.php
@@ -8,9 +8,6 @@
 
 use flight\core\Dispatcher;
 
-require_once 'vendor/autoload.php';
-require_once __DIR__ . '/classes/Hello.php';
-
 class DispatcherTest extends PHPUnit\Framework\TestCase
 {
     /**
@@ -47,6 +44,61 @@ class DispatcherTest extends PHPUnit\Framework\TestCase
         self::assertEquals('hello', $result);
     }
 
+    public function testHasEvent()
+    {
+        $this->dispatcher->set('map-event', function () {
+            return 'hello';
+        });
+
+		$result = $this->dispatcher->has('map-event');
+
+		$this->assertTrue($result);
+    }
+
+	public function testClearAllRegisteredEvents() {
+		$this->dispatcher->set('map-event', function () {
+			return 'hello';
+		});
+
+		$this->dispatcher->set('map-event-2', function () {
+			return 'there';
+		});
+
+		$this->dispatcher->clear();
+
+		$result = $this->dispatcher->has('map-event');
+		$this->assertFalse($result);
+		$result = $this->dispatcher->has('map-event-2');
+		$this->assertFalse($result);
+	}
+
+	public function testClearDeclaredRegisteredEvent() {
+		$this->dispatcher->set('map-event', function () {
+			return 'hello';
+		});
+
+		$this->dispatcher->set('map-event-2', function () {
+			return 'there';
+		});
+
+		$this->dispatcher->clear('map-event');
+
+		$result = $this->dispatcher->has('map-event');
+		$this->assertFalse($result);
+		$result = $this->dispatcher->has('map-event-2');
+		$this->assertTrue($result);
+	}
+
+	// Map a static function
+	public function testStaticFunctionMapping()
+	{
+		$this->dispatcher->set('map2', 'Hello::sayHi');
+
+		$result = $this->dispatcher->run('map2');
+
+		self::assertEquals('hello', $result);
+	}
+
     // Map a class method
     public function testClassMethodMapping()
     {
@@ -98,4 +150,31 @@ class DispatcherTest extends PHPUnit\Framework\TestCase
 
         $this->dispatcher->execute(['NonExistentClass', 'nonExistentMethod']);
     }
+
+	public function testCallFunction4Params() {
+		$closure = function($param1, $params2, $params3, $param4) {
+			return 'hello'.$param1.$params2.$params3.$param4;
+		};
+		$params = ['param1', 'param2', 'param3', 'param4'];
+		$result = $this->dispatcher->callFunction($closure, $params);
+		$this->assertEquals('helloparam1param2param3param4', $result);
+	}
+
+	public function testCallFunction5Params() {
+		$closure = function($param1, $params2, $params3, $param4, $param5) {
+			return 'hello'.$param1.$params2.$params3.$param4.$param5;
+		};
+		$params = ['param1', 'param2', 'param3', 'param4', 'param5'];
+		$result = $this->dispatcher->callFunction($closure, $params);
+		$this->assertEquals('helloparam1param2param3param4param5', $result);
+	}
+
+	public function testCallFunction6Params() {
+		$closure = function($param1, $params2, $params3, $param4, $param5, $param6) {
+			return 'hello'.$param1.$params2.$params3.$param4.$param5.$param6;
+		};
+		$params = ['param1', 'param2', 'param3', 'param4', 'param5', 'param6'];
+		$result = $this->dispatcher->callFunction($closure, $params);
+		$this->assertEquals('helloparam1param2param3param4param5param6', $result);
+	}
 }
diff --git a/tests/EngineTest.php b/tests/EngineTest.php
new file mode 100644
index 0000000..cf2b13a
--- /dev/null
+++ b/tests/EngineTest.php
@@ -0,0 +1,268 @@
+
+ * @license     MIT, http://flightphp.com/license
+ */
+
+
+class EngineTest extends PHPUnit\Framework\TestCase
+{
+	public function setUp(): void {
+		$_SERVER = [];
+	}
+
+	public function tearDown(): void {
+		$_SERVER = [];
+	}
+	public function testInitBeforeStart() {
+		$engine = new class extends Engine {
+			public function getInitializedVar() {
+				return $this->initialized;
+			}
+		};
+		$this->assertTrue($engine->getInitializedVar());
+		$engine->start();
+
+		// this is necessary cause it doesn't actually send the response correctly
+		ob_end_clean();
+
+		$this->assertFalse($engine->router()->case_sensitive);
+		$this->assertTrue($engine->response()->content_length);
+	}
+
+	public function testHandleErrorNoErrorNumber() {
+		$engine = new Engine();
+		$result = $engine->handleError(0, '', '', 0);
+		$this->assertFalse($result);
+	}
+
+	public function testHandleErrorWithException() {
+		$engine = new Engine();
+		$this->expectException(Exception::class);
+		$this->expectExceptionCode(5);
+		$this->expectExceptionMessage('thrown error message');
+		$engine->handleError(5, 'thrown error message', '', 0);
+	}
+
+	public function testHandleException() {
+		$engine = new Engine();
+		$regex_message = preg_quote('500 Internal Server Error
thrown exception message (20)
');
+		$this->expectOutputRegex('~'.$regex_message.'~');
+		$engine->handleException(new Exception('thrown exception message', 20));
+	}
+
+	public function testMapExistingMethod() {
+		$engine = new Engine();
+		$this->expectException(Exception::class);
+		$this->expectExceptionMessage('Cannot override an existing framework method.');
+		$engine->map('_start', function() {});
+	}
+
+	public function testRegisterExistingMethod() {
+		$engine = new Engine();
+		$this->expectException(Exception::class);
+		$this->expectExceptionMessage('Cannot override an existing framework method.');
+		$engine->register('_error', 'stdClass');
+	}
+
+	public function testSetArrayOfValues() {
+		$engine = new Engine();
+		$engine->set([ 'key1' => 'value1', 'key2' => 'value2']);
+		$this->assertEquals('value1', $engine->get('key1'));
+		$this->assertEquals('value2', $engine->get('key2'));
+	}
+
+	public function testStartWithRoute() {
+		$_SERVER['REQUEST_METHOD'] = 'GET';
+		$_SERVER['REQUEST_URI'] = '/someRoute';
+
+		$engine = new class extends Engine {
+			public function getInitializedVar() {
+				return $this->initialized;
+			}
+		};
+		$engine->route('/someRoute', function() { echo 'i ran'; }, true);
+		$this->expectOutputString('i ran');
+		$engine->start();
+	}
+
+	// n0nag0n - I don't know why this does what it does, but it's existing framework functionality 1/1/24
+	public function testStartWithRouteButReturnedValueThrows404() {
+		$_SERVER['REQUEST_METHOD'] = 'GET';
+		$_SERVER['REQUEST_URI'] = '/someRoute';
+
+		$engine = new class extends Engine {
+			public function getInitializedVar() {
+				return $this->initialized;
+			}
+		};
+		$engine->route('/someRoute', function() { echo 'i ran'; return true; }, true);
+		$this->expectOutputString('404 Not Found
The page you have requested could not be found.
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                ');
+		$engine->start();
+	}
+
+	public function testStopWithCode() {
+		$engine = new class extends Engine {
+			public function getLoader() {
+				return $this->loader;
+			}
+		};
+		// doing this so we can overwrite some parts of the response
+		$engine->getLoader()->register('response', function() {
+			return new class extends \flight\net\Response {
+				public function __construct() {}
+				public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): Response
+				{
+					return $this;
+				}
+			};
+		});
+		// need to add another one of these because _stop() stops and gets clean, but $response->send() does too.....
+		ob_start();
+		$engine->response()->write('I am a teapot');
+		$this->expectOutputString('I am a teapot');
+		$engine->stop(500);
+		$this->assertEquals(500, $engine->response()->status());
+	}
+
+	public function testPostRoute() {
+		$engine = new Engine();
+		$engine->post('/someRoute', function() { echo 'i ran'; }, true);
+		$routes = $engine->router()->getRoutes();
+		$this->assertEquals('POST', $routes[0]->methods[0]);
+		$this->assertEquals('/someRoute', $routes[0]->pattern);
+	}
+
+	public function testPutRoute() {
+		$engine = new Engine();
+		$engine->put('/someRoute', function() { echo 'i ran'; }, true);
+		$routes = $engine->router()->getRoutes();
+		$this->assertEquals('PUT', $routes[0]->methods[0]);
+		$this->assertEquals('/someRoute', $routes[0]->pattern);
+	}
+
+	public function testPatchRoute() {
+		$engine = new Engine();
+		$engine->patch('/someRoute', function() { echo 'i ran'; }, true);
+		$routes = $engine->router()->getRoutes();
+		$this->assertEquals('PATCH', $routes[0]->methods[0]);
+		$this->assertEquals('/someRoute', $routes[0]->pattern);
+	}
+
+	public function testDeleteRoute() {
+		$engine = new Engine();
+		$engine->delete('/someRoute', function() { echo 'i ran'; }, true);
+		$routes = $engine->router()->getRoutes();
+		$this->assertEquals('DELETE', $routes[0]->methods[0]);
+		$this->assertEquals('/someRoute', $routes[0]->pattern);
+	}
+
+	public function testHalt() {
+		$engine = new class extends Engine {
+			public function getLoader() {
+				return $this->loader;
+			}
+		};
+		// doing this so we can overwrite some parts of the response
+		$engine->getLoader()->register('response', function() {
+			return new class extends \flight\net\Response {
+				public function __construct() {}
+				public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): Response
+				{
+					return $this;
+				}
+			};
+		});
+		$this->expectOutputString('skip---exit');
+		$engine->halt(500, 'skip---exit');
+		$this->assertEquals(500, $engine->response()->status());
+	}
+
+	public function testRedirect() {
+		$engine = new Engine();
+		$engine->redirect('https://github.com', 302);
+		$this->assertEquals('https://github.com', $engine->response()->headers()['Location']);
+		$this->assertEquals(302, $engine->response()->status());
+	}
+
+	public function testRedirectWithBaseUrl() {
+		$engine = new Engine();
+		$engine->set('flight.base_url', '/subdirectory');
+		$engine->redirect('/someRoute', 301);
+		$this->assertEquals('/subdirectory/someRoute', $engine->response()->headers()['Location']);
+		$this->assertEquals(301, $engine->response()->status());
+	}
+
+	public function testJson() {
+		$engine = new Engine();
+		$engine->json(['key1' => 'value1', 'key2' => 'value2']);
+		$this->expectOutputString('{"key1":"value1","key2":"value2"}');
+		$this->assertEquals('application/json; charset=utf-8', $engine->response()->headers()['Content-Type']);
+		$this->assertEquals(200, $engine->response()->status());
+	}
+
+	public function testJsonP() {
+		$engine = new Engine();
+		$engine->request()->query['jsonp'] = 'whatever';
+		$engine->jsonp(['key1' => 'value1', 'key2' => 'value2']);
+		$this->expectOutputString('whatever({"key1":"value1","key2":"value2"});');
+		$this->assertEquals('application/javascript; charset=utf-8', $engine->response()->headers()['Content-Type']);
+		$this->assertEquals(200, $engine->response()->status());
+	}
+
+	public function testJsonPBadParam() {
+		$engine = new Engine();
+		$engine->jsonp(['key1' => 'value1', 'key2' => 'value2']);
+		$this->expectOutputString('({"key1":"value1","key2":"value2"});');
+		$this->assertEquals('application/javascript; charset=utf-8', $engine->response()->headers()['Content-Type']);
+		$this->assertEquals(200, $engine->response()->status());
+	}
+
+	public function testEtagSimple() {
+		$engine = new Engine();
+		$engine->etag('etag');
+		$this->assertEquals('etag', $engine->response()->headers()['ETag']);
+	}
+
+	public function testEtagWithHttpIfNoneMatch() {
+		// just need this not to exit...
+		$engine = new class extends Engine {
+			public function _halt(int $code = 200, string $message = ''): void
+			{
+				$this->response()->status($code);
+				$this->response()->write($message);
+			}
+		};
+		$_SERVER['HTTP_IF_NONE_MATCH'] = 'etag';
+		$engine->etag('etag');
+		$this->assertEquals('etag', $engine->response()->headers()['ETag']);
+		$this->assertEquals(304, $engine->response()->status());
+	}
+
+	public function testLastModifiedSimple() {
+		$engine = new Engine();
+		$engine->lastModified(1234567890);
+		$this->assertEquals('Fri, 13 Feb 2009 23:31:30 GMT', $engine->response()->headers()['Last-Modified']);
+	}
+
+	public function testLastModifiedWithHttpIfModifiedSince() {
+		// just need this not to exit...
+		$engine = new class extends Engine {
+			public function _halt(int $code = 200, string $message = ''): void
+			{
+				$this->response()->status($code);
+				$this->response()->write($message);
+			}
+		};
+		$_SERVER['HTTP_IF_MODIFIED_SINCE'] = 'Fri, 13 Feb 2009 23:31:30 GMT';
+		$engine->lastModified(1234567890);
+		$this->assertEquals('Fri, 13 Feb 2009 23:31:30 GMT', $engine->response()->headers()['Last-Modified']);
+		$this->assertEquals(304, $engine->response()->status());
+	}
+}
diff --git a/tests/FilterTest.php b/tests/FilterTest.php
index 137bb61..9681a62 100644
--- a/tests/FilterTest.php
+++ b/tests/FilterTest.php
@@ -8,9 +8,6 @@
 
 use flight\Engine;
 
-require_once 'vendor/autoload.php';
-require_once __DIR__ . '/../flight/autoload.php';
-
 class FilterTest extends PHPUnit\Framework\TestCase
 {
     /**
diff --git a/tests/FlightTest.php b/tests/FlightTest.php
index 19dfdd3..588ebb2 100644
--- a/tests/FlightTest.php
+++ b/tests/FlightTest.php
@@ -11,16 +11,20 @@ use flight\template\View;
  * @copyright   Copyright (c) 2012, Mike Cao 
  * @license     MIT, http://flightphp.com/license
  */
-require_once 'vendor/autoload.php';
-require_once __DIR__ . '/../flight/Flight.php';
-
 class FlightTest extends PHPUnit\Framework\TestCase
 {
     protected function setUp(): void
     {
+		$_SERVER = [];
+		$_REQUEST = [];
         Flight::init();
     }
 
+	protected function tearDown(): void {
+		unset($_REQUEST);
+		unset($_SERVER);
+	}
+
     // Checks that default components are loaded
     public function testDefaultComponents()
     {
diff --git a/tests/LoaderTest.php b/tests/LoaderTest.php
index 2e2de8f..ecadd0a 100644
--- a/tests/LoaderTest.php
+++ b/tests/LoaderTest.php
@@ -8,9 +8,9 @@
 
 use flight\core\Loader;
 
-require_once 'vendor/autoload.php';
 require_once __DIR__ . '/classes/User.php';
 require_once __DIR__ . '/classes/Factory.php';
+require_once __DIR__ . '/classes/TesterClass.php';
 
 class LoaderTest extends PHPUnit\Framework\TestCase
 {
@@ -118,4 +118,42 @@ class LoaderTest extends PHPUnit\Framework\TestCase
         self::assertIsObject($obj);
         self::assertInstanceOf(Factory::class, $obj);
     }
+
+	public function testUnregisterClass() {
+		$this->loader->register('g', 'User');
+		$current_class = $this->loader->get('g');
+		$this->assertEquals([ 'User', [], null ], $current_class);
+		$this->loader->unregister('g');
+		$unregistered_class_result = $this->loader->get('g');
+		$this->assertNull($unregistered_class_result);
+	}
+
+	public function testNewInstance6Params() {
+		$TesterClass = $this->loader->newInstance('TesterClass', ['Bob','Fred', 'Joe', 'Jane', 'Sally', 'Suzie']);
+		$this->assertEquals('Bob', $TesterClass->param1);
+		$this->assertEquals('Fred', $TesterClass->param2);
+		$this->assertEquals('Joe', $TesterClass->param3);
+		$this->assertEquals('Jane', $TesterClass->param4);
+		$this->assertEquals('Sally', $TesterClass->param5);
+		$this->assertEquals('Suzie', $TesterClass->param6);
+	}
+
+	public function testNewInstance6ParamsBadClass() {
+		$this->expectException(Exception::class);
+		$this->expectExceptionMessage('Cannot instantiate BadClass');
+		$TesterClass = $this->loader->newInstance('BadClass', ['Bob','Fred', 'Joe', 'Jane', 'Sally', 'Suzie']);
+	}
+
+	public function testAddDirectoryAsArray() {
+		$loader = new class extends Loader {
+			public function getDirectories() {
+				return self::$dirs;
+			}
+		};
+		$loader->addDirectory([__DIR__ . '/classes']);
+		self::assertEquals([
+			dirname(__DIR__),
+			__DIR__ . '/classes'
+		], $loader->getDirectories());
+	}
 }
diff --git a/tests/MapTest.php b/tests/MapTest.php
index 5b0d3df..4f4a0eb 100644
--- a/tests/MapTest.php
+++ b/tests/MapTest.php
@@ -8,8 +8,6 @@
 
 use flight\Engine;
 
-require_once 'vendor/autoload.php';
-require_once __DIR__ . '/../flight/autoload.php';
 require_once __DIR__ . '/classes/Hello.php';
 
 class MapTest extends PHPUnit\Framework\TestCase
diff --git a/tests/RedirectTest.php b/tests/RedirectTest.php
index 0f8079a..9099fe4 100644
--- a/tests/RedirectTest.php
+++ b/tests/RedirectTest.php
@@ -8,9 +8,6 @@
 
 use flight\Engine;
 
-require_once 'vendor/autoload.php';
-require_once __DIR__ . '/../flight/autoload.php';
-
 class RedirectTest extends PHPUnit\Framework\TestCase
 {
     private Engine $app;
diff --git a/tests/RegisterTest.php b/tests/RegisterTest.php
index dc383cd..bd67106 100644
--- a/tests/RegisterTest.php
+++ b/tests/RegisterTest.php
@@ -8,8 +8,6 @@
 
 use flight\Engine;
 
-require_once 'vendor/autoload.php';
-require_once __DIR__ . '/../flight/autoload.php';
 require_once __DIR__ . '/classes/User.php';
 
 class RegisterTest extends PHPUnit\Framework\TestCase
diff --git a/tests/RenderTest.php b/tests/RenderTest.php
index 260caa6..dccdc69 100644
--- a/tests/RenderTest.php
+++ b/tests/RenderTest.php
@@ -8,9 +8,6 @@
 
 use flight\Engine;
 
-require_once 'vendor/autoload.php';
-require_once __DIR__ . '/../flight/Flight.php';
-
 class RenderTest extends PHPUnit\Framework\TestCase
 {
     private Engine $app;
diff --git a/tests/RequestTest.php b/tests/RequestTest.php
index 842c1bc..aea65ee 100644
--- a/tests/RequestTest.php
+++ b/tests/RequestTest.php
@@ -7,9 +7,7 @@
  */
 
 use flight\net\Request;
-
-require_once 'vendor/autoload.php';
-require_once __DIR__ . '/../flight/autoload.php';
+use flight\util\Collection;
 
 class RequestTest extends PHPUnit\Framework\TestCase
 {
@@ -17,6 +15,8 @@ class RequestTest extends PHPUnit\Framework\TestCase
 
     protected function setUp(): void
     {
+		$_SERVER = [];
+		$_REQUEST = [];
         $_SERVER['REQUEST_URI'] = '/';
         $_SERVER['SCRIPT_NAME'] = '/index.php';
         $_SERVER['REQUEST_METHOD'] = 'GET';
@@ -34,6 +34,11 @@ class RequestTest extends PHPUnit\Framework\TestCase
         $this->request = new Request();
     }
 
+	protected function tearDown(): void {
+		unset($_REQUEST);
+		unset($_SERVER);
+	}
+
     public function testDefaults()
     {
         self::assertEquals('/', $this->request->url);
@@ -150,4 +155,42 @@ class RequestTest extends PHPUnit\Framework\TestCase
         $request = new Request();
         self::assertEquals('http', $request->scheme);
     }
+
+	public function testInitUrlSameAsBaseDirectory() {
+		$request = new Request([
+			'url' => '/vagrant/public/flightphp',
+			'base' => '/vagrant/public',
+			'query' => new Collection(),
+			'type' => ''
+		]);
+		$this->assertEquals('/flightphp', $request->url);
+	}
+
+	public function testInitNoUrl() {
+		$request = new Request([
+			'url' => '',
+			'base' => '/vagrant/public',
+			'type' => ''
+		]);
+		$this->assertEquals('/', $request->url);
+	}
+
+	public function testInitWithJsonBody() {
+		// create dummy file to pull request body from
+		$tmpfile = tmpfile();
+		$stream_path = stream_get_meta_data($tmpfile)['uri'];
+		file_put_contents($stream_path, '{"foo":"bar"}');
+		$_SERVER['REQUEST_METHOD'] = 'POST';
+		$request = new Request([
+			'url' => '/something/fancy',
+			'base' => '/vagrant/public',
+			'type' => 'application/json',
+			'length' => 13,
+			'data' => new Collection(),
+			'query' => new Collection(),
+			'stream_path' => $stream_path
+		]);
+		$this->assertEquals([ 'foo' => 'bar' ], $request->data->getData());
+		$this->assertEquals('{"foo":"bar"}', $request->getBody());
+	}
 }
diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php
new file mode 100644
index 0000000..bbcdb62
--- /dev/null
+++ b/tests/ResponseTest.php
@@ -0,0 +1,227 @@
+
+ * @license     MIT, http://flightphp.com/license
+ */
+
+use flight\net\Request;
+use flight\net\Response;
+use flight\util\Collection;
+
+class ResponseTest extends PHPUnit\Framework\TestCase
+{
+
+    protected function setUp(): void
+    {
+		$_SERVER = [];
+		$_REQUEST = [];
+        $_GET = [];
+        $_POST = [];
+        $_COOKIE = [];
+        $_FILES = [];
+    }
+
+	protected function tearDown(): void {
+		unset($_REQUEST);
+		unset($_SERVER);
+	}
+
+    public function testStatusDefault() {
+		$response = new Response();
+		$this->assertSame(200, $response->status());
+	}
+
+	public function testStatusValidCode() {
+		$response = new Response();
+		$response->status(200);
+		$this->assertEquals(200, $response->status());
+	}
+
+	public function testStatusInvalidCode() {
+		$response = new Response();
+		$this->expectException(Exception::class);
+		$this->expectExceptionMessage('Invalid status code.');
+		$response->status(999);
+	}
+
+	public function testStatusReturnObject() {
+		$response = new Response();
+		$this->assertEquals($response, $response->status(200));
+	}
+
+	public function testHeaderSingle() {
+		$response = new Response();
+		$response->header('Content-Type', 'text/html');
+		$this->assertEquals(['Content-Type' => 'text/html'], $response->headers());
+	}
+
+	public function testHeaderSingleKeepCaseSensitive() {
+		$response = new Response();
+		$response->header('content-type', 'text/html');
+		$response->header('x-test', 'test');
+		$this->assertEquals(['content-type' => 'text/html', 'x-test' => 'test'], $response->headers());
+	}
+	
+	public function testHeaderArray() {
+		$response = new Response();
+		$response->header(['Content-Type' => 'text/html', 'X-Test' => 'test']);
+		$this->assertEquals(['Content-Type' => 'text/html', 'X-Test' => 'test'], $response->headers());
+	}
+
+	public function testHeaderReturnObject() {
+		$response = new Response();
+		$this->assertEquals($response, $response->header('Content-Type', 'text/html'));
+	}
+
+	public function testWrite() {
+		$response = new class extends Response {
+			public function getBody() {
+				return $this->body;
+			}
+		};
+		$response->write('test');
+		$this->assertEquals('test', $response->getBody());
+	}
+
+	public function testWriteEmptyString() {
+		$response = new class extends Response {
+			public function getBody() {
+				return $this->body;
+			}
+		};
+		$response->write('');
+		$this->assertEquals('', $response->getBody());
+	}
+
+	public function testWriteReturnObject() {
+		$response = new Response();
+		$this->assertEquals($response, $response->write('test'));
+	}
+
+	public function testClear() {
+		$response = new class extends Response {
+			public function getBody() {
+				return $this->body;
+			}
+		};
+		$response->write('test');
+		$response->status(404);
+		$response->header('Content-Type', 'text/html');
+		$response->clear();
+		$this->assertEquals('', $response->getBody());
+		$this->assertEquals(200, $response->status());
+		$this->assertEquals([], $response->headers());
+	}
+
+	public function testCacheSimple() {
+		$response = new Response();
+		$cache_time = time() + 60;
+		$response->cache($cache_time);
+		$this->assertEquals([
+			'Expires' => gmdate('D, d M Y H:i:s', $cache_time) . ' GMT',
+			'Cache-Control' => 'max-age=60'
+		], $response->headers());
+	}
+
+	public function testCacheSimpleWithString() {
+		$response = new Response();
+		$cache_time = time() + 60;
+		$response->cache('now +60 seconds');
+		$this->assertEquals([
+			'Expires' => gmdate('D, d M Y H:i:s', $cache_time) . ' GMT',
+			'Cache-Control' => 'max-age=60'
+		], $response->headers());
+	}
+
+	public function testCacheSimpleWithPragma() {
+		$response = new Response();
+		$cache_time = time() + 60;
+		$response->header('Pragma', 'no-cache');
+		$response->cache($cache_time);
+		$this->assertEquals([
+			'Expires' => gmdate('D, d M Y H:i:s', $cache_time) . ' GMT',
+			'Cache-Control' => 'max-age=60'
+		], $response->headers());
+	}
+
+	public function testCacheFalseExpiresValue() {
+		$response = new Response();
+		$response->cache(false);
+		$this->assertEquals([
+			'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
+			'Cache-Control' => [
+                'no-store, no-cache, must-revalidate',
+                'post-check=0, pre-check=0',
+                'max-age=0',
+            ],
+			'Pragma' => 'no-cache'
+		], $response->headers());
+	}
+
+	public function testSendHeadersRegular() {
+		$response = new class extends Response {
+			protected $test_sent_headers = [];
+
+			protected array $headers = [
+				'Cache-Control' => [
+					'no-store, no-cache, must-revalidate',
+					'post-check=0, pre-check=0',
+					'max-age=0',
+				]
+			];
+			public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): Response
+			{
+				$this->test_sent_headers[] = $header_string;
+				return $this;
+			}
+
+			public function getSentHeaders(): array
+			{
+				return $this->test_sent_headers;
+			}
+		};
+		$response->header('Content-Type', 'text/html');
+		$response->header('X-Test', 'test');
+		$response->write('Something');
+
+		$response->sendHeaders();
+		$sent_headers = $response->getSentHeaders();
+		$this->assertEquals([
+			'HTTP/1.1 200 OK',
+			'Cache-Control: no-store, no-cache, must-revalidate',
+			'Cache-Control: post-check=0, pre-check=0',
+			'Cache-Control: max-age=0',
+			'Content-Type: text/html',
+			'X-Test: test',
+			'Content-Length: 9'
+		], $sent_headers);
+	}
+
+	public function testSentDefault() {
+		$response = new Response();
+		$this->assertFalse($response->sent());
+	}
+
+	public function testSentTrue() {
+		$response = new class extends Response {
+			protected $test_sent_headers = [];
+
+			public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): Response
+			{
+				$this->test_sent_headers[] = $header_string;
+				return $this;
+			}
+		};
+		$response->header('Content-Type', 'text/html');
+		$response->header('X-Test', 'test');
+		$response->write('Something');
+
+		$this->expectOutputString('Something');
+		$response->send();
+		$this->assertTrue($response->sent());
+	}
+
+
+}
diff --git a/tests/RouterTest.php b/tests/RouterTest.php
index 686d91f..9adc7fc 100644
--- a/tests/RouterTest.php
+++ b/tests/RouterTest.php
@@ -10,9 +10,6 @@ use flight\core\Dispatcher;
 use flight\net\Request;
 use flight\net\Router;
 
-require_once 'vendor/autoload.php';
-require_once __DIR__ . '/../flight/autoload.php';
-
 class RouterTest extends PHPUnit\Framework\TestCase
 {
     private Router $router;
@@ -23,11 +20,18 @@ class RouterTest extends PHPUnit\Framework\TestCase
 
     protected function setUp(): void
     {
+		$_SERVER = [];
+		$_REQUEST = [];
         $this->router = new Router();
         $this->request = new Request();
         $this->dispatcher = new Dispatcher();
     }
 
+	protected function tearDown(): void {
+		unset($_REQUEST);
+		unset($_SERVER);
+	}
+
     // Simple output
     public function ok()
     {
@@ -101,6 +105,16 @@ class RouterTest extends PHPUnit\Framework\TestCase
         $this->check('OK');
     }
 
+	// Simple path with trailing slash
+	// Simple path
+    public function testPathRouteTrailingSlash()
+    {
+        $this->router->map('/path/', [$this, 'ok']);
+        $this->request->url = '/path';
+
+        $this->check('OK');
+    }
+
     // POST route
     public function testPostRoute()
     {
@@ -293,4 +307,49 @@ class RouterTest extends PHPUnit\Framework\TestCase
 
         $this->check('цветя');
     }
+
+	public function testGetAndClearRoutes() {
+		$this->router->map('/path1', [$this, 'ok']);
+		$this->router->map('/path2', [$this, 'ok']);
+		$this->router->map('/path3', [$this, 'ok']);
+		$this->router->map('/path4', [$this, 'ok']);
+		$this->router->map('/path5', [$this, 'ok']);
+		$this->router->map('/path6', [$this, 'ok']);
+		$this->router->map('/path7', [$this, 'ok']);
+		$this->router->map('/path8', [$this, 'ok']);
+		$this->router->map('/path9', [$this, 'ok']);
+		
+		$routes = $this->router->getRoutes();
+		$this->assertEquals(9, count($routes));
+
+		$this->router->clear();
+
+		$this->assertEquals(0, count($this->router->getRoutes()));
+	}
+
+	public function testResetRoutes() {
+		$router = new class extends Router {
+			public function getIndex() {
+				return $this->index;
+			}
+		};
+
+		$router->map('/path1', [$this, 'ok']);
+		$router->map('/path2', [$this, 'ok']);
+		$router->map('/path3', [$this, 'ok']);
+		$router->map('/path4', [$this, 'ok']);
+		$router->map('/path5', [$this, 'ok']);
+		$router->map('/path6', [$this, 'ok']);
+		$router->map('/path7', [$this, 'ok']);
+		$router->map('/path8', [$this, 'ok']);
+		$router->map('/path9', [$this, 'ok']);
+		
+		$router->next();
+		$router->next();
+		$router->next();
+
+		$this->assertEquals(3, $router->getIndex());
+		$router->reset();
+		$this->assertEquals(0, $router->getIndex());
+	}
 }
diff --git a/tests/VariableTest.php b/tests/VariableTest.php
index e7a610d..03f2900 100644
--- a/tests/VariableTest.php
+++ b/tests/VariableTest.php
@@ -5,9 +5,6 @@
  * @copyright   Copyright (c) 2012, Mike Cao 
  * @license     MIT, http://flightphp.com/license
  */
-require_once 'vendor/autoload.php';
-require_once __DIR__ . '/../flight/autoload.php';
-
 class VariableTest extends PHPUnit\Framework\TestCase
 {
     /**
diff --git a/tests/ViewTest.php b/tests/ViewTest.php
index bdfcfeb..2013861 100644
--- a/tests/ViewTest.php
+++ b/tests/ViewTest.php
@@ -5,9 +5,6 @@
  * @copyright   Copyright (c) 2012, Mike Cao 
  * @license     MIT, http://flightphp.com/license
  */
-require_once 'vendor/autoload.php';
-require_once __DIR__ . '/../flight/autoload.php';
-
 class ViewTest extends PHPUnit\Framework\TestCase
 {
     /**
@@ -36,6 +33,21 @@ class ViewTest extends PHPUnit\Framework\TestCase
         $this->assertNull($this->view->get('test'));
     }
 
+	public function testMultipleVariables() {
+		$this->view->set([
+			'test' => 123,
+			'foo' => 'bar'
+		]);
+
+		$this->assertEquals(123, $this->view->get('test'));
+		$this->assertEquals('bar', $this->view->get('foo'));
+
+		$this->view->clear();
+
+		$this->assertNull($this->view->get('test'));
+		$this->assertNull($this->view->get('foo'));
+	}
+
     // Check if template files exist
     public function testTemplateExists()
     {
@@ -51,6 +63,13 @@ class ViewTest extends PHPUnit\Framework\TestCase
         $this->expectOutputString('Hello, Bob!');
     }
 
+	public function testRenderBadFilePath() {
+		$this->expectException(Exception::class);
+		$this->expectExceptionMessage('Template file not found: '.__DIR__ . '/views/badfile.php');
+
+		$this->view->render('badfile');
+	}
+
     // Fetch template output
     public function testFetch()
     {
@@ -79,4 +98,23 @@ class ViewTest extends PHPUnit\Framework\TestCase
 
         $this->expectOutputString('Hello world, Bob!');
     }
+
+	public function testGetTemplateAbsolutePath() {
+		$tmpfile = tmpfile();
+		$this->view->extension = '';
+		$file_path = stream_get_meta_data($tmpfile)['uri'];
+		$this->assertEquals($file_path, $this->view->getTemplate($file_path));
+	}
+
+	public function testE() {
+		$this->expectOutputString('<script>');
+		$result = $this->view->e('