← Back to team overview

kolibri-discuss team mailing list archive

[Merge] lp:~stian-prestholdt/kolibri/test-framework into lp:kolibri

 

Anders Steinlein has proposed merging lp:~stian-prestholdt/kolibri/test-framework into lp:kolibri.

Requested reviews:
    Kolibri Dev (kolibri-dev)
-- 
https://code.launchpad.net/~stian-prestholdt/kolibri/test-framework/+merge/6010
Your team Kolibri Discuss is subscribed to branch lp:kolibri.
=== removed file 'examples/wishlist/conf/development.ini'
--- examples/wishlist/conf/development.ini	2009-04-14 21:48:11 +0000
+++ examples/wishlist/conf/development.ini	1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-; Empty for this simple example application.
\ No newline at end of file

=== added file 'examples/wishlist/kolibri.php'
--- examples/wishlist/kolibri.php	1970-01-01 00:00:00 +0000
+++ examples/wishlist/kolibri.php	2009-04-27 13:27:48 +0000
@@ -0,0 +1,41 @@
+<?php
+/**
+ * The HTTP gateway to the Kolibri framework. Essential core files are included, config
+ * initialized and processing of the HTTP request is started.
+ * The constants defined here, ROOT and APP_PATH, can be changed from their default values by
+ * using SetEnv in the .htaccess file or in an appropriate Apache config directive.
+ */
+
+/*
+ * Defines the root directory of the Kolibri framework. By default this is a directory named
+ * 'kolibri' within the document root.
+ */
+if ($rootDir = getenv('KOLIBRI_ROOT')) {
+	define('ROOT', $rootDir);
+}
+else {
+	define('ROOT', dirname(__FILE__) . '/kolibri');
+}
+
+/*
+ * Defines the root directory for the application. By default this is the same directory as
+ * this kolibri.php file.
+ */
+if ($appDir = getenv('KOLIBRI_APP')) {
+	define('APP_PATH', $appDir);
+}
+else {
+	define('APP_PATH', dirname(__FILE__));
+}
+
+// Require essential files. Others are loaded as needed.
+require(ROOT . '/core/Config.php');
+require(ROOT . '/core/RequestProcessor.php');
+
+// Init configuration
+Config::getInstance();
+
+$request = new Request($_GET, $_POST);
+$kolibri = new RequestProcessor($request);
+$kolibri->process();
+?>

=== modified file 'examples/wishlist/models/dao/ItemDao.php'
--- examples/wishlist/models/dao/ItemDao.php	2008-12-04 01:32:42 +0000
+++ examples/wishlist/models/dao/ItemDao.php	2009-04-20 12:53:54 +0000
@@ -18,7 +18,7 @@
 SQL;
 		return $db->getObject('Item', $query, $id); // Gets one object of type Item
 	}
-
+	
 	/**
 	 * findAll() is here used to retrieve, well, all [relevant] objects.
 	 */
@@ -50,7 +50,11 @@
 	public static function update ($item) {
 		$db = DatabaseFactory::getConnection();
 		$query = <<<SQL
-			UPDATE items SET received = :received WHERE name = :name
+			UPDATE items SET received = :received,
+							 name = :name,
+							 description = :description,
+							 price = :price
+						WHERE name = :name
 SQL;
 		return $db->query($query, $item);
 	}

=== added directory 'examples/wishlist/specs'
=== added file 'examples/wishlist/specs/SpecHelper.php'
--- examples/wishlist/specs/SpecHelper.php	1970-01-01 00:00:00 +0000
+++ examples/wishlist/specs/SpecHelper.php	2009-04-28 06:14:00 +0000
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Loads all the correct files and mode for KolibriTestCase
+ *
+ * REMEMBER to require this file in every spec class you have
+ * <code>require_once(dirname(__FILE__) . '/../SpecHelper.php')</code>
+ *
+ * Defines the root directory of the Kolibri framework. By default this is a directory named
+ * 'kolibri' within the document root.
+ */
+if (!defined('ROOT')) {
+	if (!$rootDir = getenv('KOLIBRI_ROOT')) {
+		throw new Exception('Environment variable KOLIBRI_ROOT must be defined.');
+		exit;
+	}
+	define('ROOT', $rootDir);
+
+	/*
+	 * Defines the root directory for the application. By default this is the same directory as
+	 * this kolibri.php file.
+	 */
+	$dirname = dirname(__FILE__);
+	if (basename($dirname) == 'specs') {
+		$path = dirname(__FILE__) . '/..';
+	}
+	else $path = dirname(__FILE__) . '/../..';
+
+	define('APP_PATH', $path);
+}
+
+putenv('KOLIBRI_MODE=test');
+require(ROOT . '/core/Config.php');
+require(ROOT . '/core/RequestProcessor.php');
+
+Config::getInstance();
+
+require(ROOT . '/specs/Fixtures.php');
+require(ROOT . '/specs/KolibriContext.php');
+
+$setupFile = APP_PATH . '/specs/setup.sql';
+$schemaFile = APP_PATH . '/config/schema.sql';
+
+if (file_exists($setupFile)) {
+	$db = DatabaseFactory::getConnection();
+	
+	if(file_exists($schemaFile)) {
+		$schemaContents = file_get_contents($schemaFile);
+		$db->batchQuery($schemaContents);
+		$db->commit();
+	}
+	
+	$setupContents = file_get_contents($setupFile);
+	$db->batchQuery($setupContents);
+	$db->commit();
+}
+?>

=== added directory 'examples/wishlist/specs/actions'
=== added file 'examples/wishlist/specs/actions/DescribeIndexAction.php'
--- examples/wishlist/specs/actions/DescribeIndexAction.php	1970-01-01 00:00:00 +0000
+++ examples/wishlist/specs/actions/DescribeIndexAction.php	2009-04-28 06:28:42 +0000
@@ -0,0 +1,16 @@
+<?php
+require_once(dirname(__FILE__) . '/../SpecHelper.php');
+
+class DescribeIndexAction extends KolibriContext {
+	public function itShouldReturnXsltResponse () {
+		$this->get('/');
+		$this->spec($this->response)->should->beAnInstanceOf('XsltResponse');
+	}
+
+	public function itShouldHaveModelSetWhenInSession () {
+		$item = Models::getModel(new Item());
+		$this->get('/', array(), array('model' => $item));
+		$this->spec($this->action->model)->should->beAnInstanceOf('ModelProxy');
+	}
+}
+?>
\ No newline at end of file

=== added directory 'examples/wishlist/specs/actions/items'
=== added file 'examples/wishlist/specs/actions/items/DescribeItemAddAction.php'
--- examples/wishlist/specs/actions/items/DescribeItemAddAction.php	1970-01-01 00:00:00 +0000
+++ examples/wishlist/specs/actions/items/DescribeItemAddAction.php	2009-04-28 06:28:42 +0000
@@ -0,0 +1,21 @@
+<?php
+require_once(dirname(__FILE__) . '/../../SpecHelper.php');
+
+class DescribeItemAddAction extends KolibriContext {
+	public function itShouldSetModelInSessionWhenParamsAreInvalid () {
+		$this->post('/items/add');
+		$this->spec($this->request->session->get('model'))->should->beAnInstanceOf('ModelProxy');
+	}
+
+	public function itShouldHaveInvalidModelWhenParamsAreInvalid () {
+		$this->post('/items/add');
+		$this->spec($this->action->model)->shouldNot->beValid();
+	}
+
+	public function itShouldHaveValidModelWhenParamsAreValid () {
+		$this->post('/items/add', array('name' => 'A thing'));
+		$this->spec($this->action->model)->should->validate();
+		$this->spec($this->action->msg->getMessage())->should->be('Item successfully added.');
+	}
+}
+?>

=== added directory 'examples/wishlist/specs/fixtures'
=== added file 'examples/wishlist/specs/fixtures/Item.ini'
--- examples/wishlist/specs/fixtures/Item.ini	1970-01-01 00:00:00 +0000
+++ examples/wishlist/specs/fixtures/Item.ini	2009-04-20 11:13:57 +0000
@@ -0,0 +1,17 @@
+; fixture for item model
+
+[ValidItem]
+name = Playstation 3
+description = Game console
+price = 3400
+
+[AnotherItem]
+name = Xbox 360
+description = Game console numer 33
+price = 2100
+
+
+[InvalidItem]
+name = 
+description = 
+price = 

=== added directory 'examples/wishlist/specs/models'
=== added file 'examples/wishlist/specs/models/DescribeItemModel.php'
--- examples/wishlist/specs/models/DescribeItemModel.php	1970-01-01 00:00:00 +0000
+++ examples/wishlist/specs/models/DescribeItemModel.php	2009-04-29 07:09:39 +0000
@@ -0,0 +1,99 @@
+<?php
+require_once(dirname(__FILE__) . '/../SpecHelper.php');
+
+/**
+ * Specification for the Item Model.
+ * 
+ * To execute something before or after etc, you have helper methods like:
+ * <code>setup()</code>, <code>posteSpec()</code>, <code>preSpec()</code>
+ * and <code>tearDown()</code> that you can use in your testcase.
+ */
+class DescribeItemModel extends KolibriContext {
+    public $itemName;
+    
+    /**
+     * This method acts as before() method from PHPSpec
+     */
+    public function setup () {
+        // This method doesnt have to be here if its blank.
+        $this->itemName = "Toy house";
+    }
+    
+    /**
+     * Checks validation for an valid item model.
+     */
+    public function itShouldValidateWithValidData () {
+        $item = $this->fixtures['ValidItem'];
+        $this->spec($item)->should->beValid();
+    }
+    
+    /**
+     * Checks validation for an invalid item model.
+     */
+    public function itShouldInvalidateWithInvalidData () {
+        $item = $this->fixtures['InvalidItem'];
+        $this->spec($item)->shouldNot->beValid();
+    }
+    
+    /**
+     * This spec will try to save an item object
+     */
+    public function itShouldBeAbleToSave () {
+        $item = $this->fixtures['ValidItem'];
+        $this->spec($item->save())->should->beEqualTo(1);
+    }
+    
+    /**
+     * This spec will try to load a saved item object
+     */
+    public function itShouldBeAbleToLoad () {
+        $item = Models::init('Item');
+        $item->objects->load($this->itemName);
+        $this->spec($item->name)->should->beEqualTo($this->itemName);
+    }
+
+    /**
+     * This spec will try to save an invalid item object
+     */
+    public function itShouldNotBeAbleToSaveAnInvalidItem () {
+        $item = $this->fixtures['InvalidItem'];
+        $this->spec($item->save())->should->beEqualTo(0);
+    }
+    
+    /**
+     * This spec will try to update an item object
+     */
+    public function itShouldBeAbleToUpdate () {
+        $item = Models::init('Item');
+        $item->objects->load($this->itemName);
+        $item->description = "Test update";
+        $this->spec($item->save())->should->beEqualTo(1);
+    
+        $item = Models::init('Item');
+        $item->objects->load($this->itemName);
+        $this->spec($item->description)->should->beEqualTo("Test update");
+    }
+    
+    /**
+     * This spec will try to delete a saved item object
+     */
+    public function itShouldBeAbleToDelete () {
+        $item = Models::init('Item');
+        $item->objects->load($this->itemName);
+        $item->delete();
+        
+        $item = Models::init('Item');
+        $item->objects->load($this->itemName);
+        $this->spec($item->name)->should->beEmpty();
+    }
+    
+    /**
+     * This method acts as after() method from PHPSpec
+     */
+    public function tearDown () {
+        unset($this->itemName);
+    }
+    
+
+}
+?>

=== added file 'examples/wishlist/specs/setup.sql'
--- examples/wishlist/specs/setup.sql	1970-01-01 00:00:00 +0000
+++ examples/wishlist/specs/setup.sql	2009-04-27 13:27:48 +0000
@@ -0,0 +1,15 @@
+DROP TABLE items;
+
+CREATE TABLE items (
+    name TEXT PRIMARY KEY NOT NULL,
+    description TEXT DEFAULT NULL,
+    price NUMERIC DEFAULT NULL,
+    added DATE NOT NULL,
+    received DATE DEFAULT NULL
+);
+
+INSERT INTO items (name, description, price, added, received)
+VALUES('Blue Bike', 'My favorite bike in the shop', 299, '2009-04-20', '2009-04-21');
+
+INSERT INTO items (name, description, price, added, received)
+VALUES('Toy house', 'Garden toy house with several floors', 1199, '2009-04-14', '2009-04-20');
\ No newline at end of file

=== added file 'examples/wishlist/test.php'
--- examples/wishlist/test.php	1970-01-01 00:00:00 +0000
+++ examples/wishlist/test.php	2009-04-27 13:27:48 +0000
@@ -0,0 +1,24 @@
+<?php
+/**
+ * PHPSpec
+ * This file will run all specs within this directory and all child-directories.
+ * TODO safeguard sjekke at mode = test
+ */
+
+
+chdir('specs');
+
+require_once 'PHPSpec.php';
+
+$options = new stdClass;
+$options->recursive = true;
+$options->specdoc = true;
+$options->reporter = 'html';
+
+ob_start();
+
+PHPSpec_Runner::run($options);
+
+ob_end_flush();
+
+?>
\ No newline at end of file

=== modified file 'src/.htaccess'
--- src/.htaccess	2008-10-20 16:41:10 +0000
+++ src/.htaccess	2009-04-16 12:05:45 +0000
@@ -5,4 +5,5 @@
 # static resources (as is recommended), this should be modified.
 RewriteRule ^favicon.ico$ static/favicon.ico [L]
 RewriteCond %{REQUEST_URI} !^/static
+RewriteCond %{REQUEST_URI} !test.php$
 RewriteRule ^(.*)$ kolibri.php

=== modified file 'src/core/Session.php'
--- src/core/Session.php	2009-04-20 16:03:03 +0000
+++ src/core/Session.php	2009-04-27 13:27:48 +0000
@@ -31,7 +31,8 @@
 	 * Actually starts the session.
 	 */
 	private function start () {
-		if (PHP_SAPI != 'cli') {
+		
+		if (PHP_SAPI != 'cli' && session_id() === '') {
 			session_start();
 		}
 		$this->started = true;

=== modified file 'src/database/PostgreSqlConnection.php' (properties changed: -x to +x)
--- src/database/PostgreSqlConnection.php	2009-04-15 22:00:10 +0000
+++ src/database/PostgreSqlConnection.php	2009-04-29 07:09:39 +0000
@@ -160,6 +160,21 @@
 
 		throw new DatabaseException('Query could not be sent to the database. Connection lost?');
 	}
+	
+	/**
+	 * Sends several queries to the database after escaping and interpolating the supplied parameters.
+	 *
+	 * TODO: we want that every batchQuery will return the same, so this method has to return the number
+	 * of changes in the database.
+	 *
+	 * @param string $query The query to execute.
+	 * @param mixed $params Parameters to interpolate into query.
+	 * @throws Exception    Upon an error when executing the query.
+	 * @return ResultSet    Representing the query results. Implementation-specific.
+	 */
+	public function batchQuery ($query, $params = null) {
+		return $this->query($query, $params);
+	}
 
 	/**
 	 * Escapes a value to make it safe for use in SQL queries.

=== modified file 'src/database/SqliteConnection.php' (properties changed: -x to +x)
--- src/database/SqliteConnection.php	2009-04-21 17:15:47 +0000
+++ src/database/SqliteConnection.php	2009-04-28 06:11:20 +0000
@@ -149,7 +149,51 @@
 		// XXX: Should perhaps pass some kind of SQL error state as code
 		throw new SqlException($error, $preparedQuery);
 	}
-
+	
+	
+	/**
+	 * Sends several queries to the database after escaping and interpolating the supplied parameters, and
+	 * returns number of changes in the database.
+	 *
+	 * If a connection to the database is not yet established, <code>connect()</code> is called
+	 * implicitly. The same is true of transactions; if a transaction has not yet been started on the
+	 * connection, <code>begin()</code> is called.
+	 *
+	 * @param string $query The query to execute.
+	 * @param mixed $params Parameters to interpolate into query.
+	 * @throws Exception    Upon an error when executing the query.
+	 * @return ResultSet    Representing the query results. Implementation-specific.
+	 */
+	public function batchQuery ($query, $params = null) {
+		if (!$this->connection) {
+			$this->connect();
+		}
+		if ($this->resultSet !== null) {
+			$this->resultSet = null;
+		}
+		if (!$this->autocommit) {
+			if ($this->transactionInError) {
+				return false;
+			}
+			else if (!$this->inTransaction) {
+				// No transaction yet started, let's begin one
+				$this->begin();
+			}
+		}
+		$preparedQuery = $this->prepareQuery($query, $params);
+		
+		$error = null;
+		
+		// returns number of changes to the database
+		if (@$this->connection->queryExec($preparedQuery, $error)) {
+			return @$this->connection->changes();
+		}
+		
+		$this->rollback();
+		// XXX: Should perhaps pass some kind of SQL error state as code
+		throw new SqlException($error, $preparedQuery);
+	}
+	
 	/**
 	 * Returns the native database connection. Used internally by <code>SqlResultSet</code> which
 	 * required the connection for some of its functionality.

=== modified file 'src/response/FileResponse.php'
--- src/response/FileResponse.php	2009-04-11 23:30:29 +0000
+++ src/response/FileResponse.php	2009-04-27 13:27:48 +0000
@@ -55,10 +55,12 @@
 
 		if ($this->dataIsFile) {
 			$this->setHeader('Content-Length', filesize($this->data));
+			$this->sendHeaders();
 			readfile($this->data);
 		}
 		else {
 			$this->setHeader('Content-Length', mb_strlen($this->data));
+			$this->sendHeaders();
 			echo $this->data;
 		}
 	}

=== modified file 'src/response/JsonResponse.php'
--- src/response/JsonResponse.php	2009-04-11 23:30:29 +0000
+++ src/response/JsonResponse.php	2009-04-27 13:27:48 +0000
@@ -18,6 +18,8 @@
 	 * Performs the JSON encoding and outputs the result.
 	 */
 	public function render ($request) {
+		$this->sendHeaders();
+		
 		echo json_encode($this->data);
 	}
 }

=== modified file 'src/response/PhpResponse.php'
--- src/response/PhpResponse.php	2009-04-16 15:11:14 +0000
+++ src/response/PhpResponse.php	2009-04-27 13:27:48 +0000
@@ -32,6 +32,9 @@
 	 * thus used as the results output.
 	 */
 	public function render ($request) {
+		
+		$this->sendHeaders();
+		
 		$data = array();
 		foreach ($this->data as $key => $value) {
 			$data[$key] = $value;

=== modified file 'src/response/RedirectResponse.php'
--- src/response/RedirectResponse.php	2009-04-14 16:50:50 +0000
+++ src/response/RedirectResponse.php	2009-04-27 13:27:48 +0000
@@ -5,7 +5,7 @@
  * recommended to use a more specific response class where one is availible.
  */
 class RedirectResponse extends Response {
-	private $location;
+	public $location;
 
 	/**
 	 * Initialize this response.
@@ -15,14 +15,17 @@
 	 */
 	public function __construct ($location, $status = 302) {
 		parent::__construct(null, $status);
-		$this->location = Config::get('webRoot') . $location;
+		$this->location = $location;
 	}
 
 	/**
 	 * Sends the redirect to the client.
 	 */
 	public function render ($request) {
-		$this->setHeader('Location', $this->location);
+		$this->setHeader('Location', Config::get('webRoot') . $this->location);
+		
+		$this->sendHeaders();
+		
 		exit;
 	}
 }

=== modified file 'src/response/Response.php' (properties changed: -x to +x)
--- src/response/Response.php	2009-04-12 16:32:29 +0000
+++ src/response/Response.php	2009-04-28 06:38:28 +0000
@@ -8,6 +8,7 @@
 	protected $status;
 	protected $contentType;
 	protected $charset;
+	protected $headerCache = array();
 	
 	/**
 	 * Initializes this response with the supplied response data and meta data. When using this
@@ -26,18 +27,17 @@
 		$this->status      = $status;
 		$this->contentType = $contentType;
 		$this->charset     = $charset;
-		$this->setHeader('Content-Type', "$contentType; charset=$charset", $status);
+		$this->setHeader('Content-Type', "$contentType; charset=$charset");
 	}
 
 	/**
-	 * Sets a header. If $status is supplied, the HTTP status code is changed to its value.
+	 * Sets a header in the headerCache array.
 	 *
 	 * @param string $header The header to set.
 	 * @param string $value  The value to set.
-	 * @param int $status    New HTTP status code to set, if any.
 	 */
-	public final function setHeader ($header, $value, $status = null) {
-		header("$header: $value", true, $status);
+	public final function setHeader ($header, $value) {
+		$this->headerCache[] = "$header: $value";
 	}
 
 	/**
@@ -74,6 +74,16 @@
 		$this->data .= $content . "\n";
 	}
 
+
+	/**
+	 * Sends out the buffered headers.
+	 */
+	protected function sendHeaders() {
+		foreach($this->headerCache as $value) {
+			header($value, true, $this->status);
+		}
+	}
+
 	/**
 	 * Outputs the response body.
 	 */

=== modified file 'src/response/SmartyResponse.php'
--- src/response/SmartyResponse.php	2009-04-11 23:30:29 +0000
+++ src/response/SmartyResponse.php	2009-04-27 13:27:48 +0000
@@ -44,6 +44,9 @@
 	 * along with the request object and application configuration is exposed to the template.
 	 */
 	public function render ($request) {
+		
+		$this->sendHeaders();
+		
 		$conf = Config::get('smarty');
 		if ($conf === null) {
 			throw new Exception('Smarty settings missing from application configuration.');

=== modified file 'src/response/XsltResponse.php'
--- src/response/XsltResponse.php	2009-04-12 16:32:29 +0000
+++ src/response/XsltResponse.php	2009-04-27 13:27:48 +0000
@@ -25,6 +25,9 @@
 	 * Generates XML of the data, and performs XSL transformation on it before outputting.
 	 */
 	public function render ($request) {
+		
+		$this->sendHeaders();
+		
 		$xmlGenerator = new XmlGenerator();
 		// Wrap request data in a containing element, <request />
 		$xmlGenerator->append($request, 'request');
@@ -41,6 +44,8 @@
 		}
 
 		echo $transformer->process($xmlGenerator->getDom());
+		
+		
 	}
 }
 ?>

=== added directory 'src/specs'
=== added file 'src/specs/Fixtures.php'
--- src/specs/Fixtures.php	1970-01-01 00:00:00 +0000
+++ src/specs/Fixtures.php	2009-04-28 06:28:42 +0000
@@ -0,0 +1,64 @@
+<?php
+/**
+ * This class loads fixtures for our test-framework. It acts like an array, so you can
+ * access all your fixture by using <code>$this->fixture['fixtureName']</code>. Each
+ * fixture is loaded as their correct model, so you can call the method <code>save()</code>
+ * etc, when ever you like.
+ */
+class Fixtures implements ArrayAccess {
+	private $fixtures = array();
+	private $modelName = null;
+	
+	/**
+	 * Creates a new instance of this class. The modelName will be saved for usage in the
+	 * <code>populate()</code> method wich is executed right away to set up the fixtures.
+	 *
+	 * @param string $modelName Name of the model.
+	 */
+	public function __construct($modelName) {
+		$this->modelName = $modelName;
+		$this->populate();
+	}
+	
+    /**
+     * Parses the <modelName>.ini file and returns an array of the objects.
+     */
+    public function populate () {
+        if ($this->modelName) {
+            $iniFile = APP_PATH . "/specs/fixtures/$this->modelName.ini";
+            
+			if (!file_exists($iniFile)) {
+				return null;
+			}
+
+            $models = parse_ini_file($iniFile, true);
+            foreach ($models as $name => $model) {
+                if (is_array($model) && !empty($name)) {
+                    $newModel = Models::init($this->modelName);
+                    
+                    foreach ($model as $key => $value) {
+                        $newModel->$key = $value;
+                    }
+                    $this->fixtures[$name] = $newModel;
+                }
+			}
+        }
+    }
+	
+	/**
+	 * Methods needed for ArrayAccess
+	 */
+    public function offsetSet ($offset, $value) {
+        $this->fixtures[$offset] = $value;
+    }
+    public function offsetExists ($offset) {
+        return isset($this->fixtures[$offset]);
+    }
+    public function offsetUnset ($offset) {
+        unset($this->fixtures[$offset]);
+    }
+    public function offsetGet ($offset) {
+        return isset($this->fixtures[$offset]) ? $this->fixtures[$offset] : null;
+    }
+}
+?>
\ No newline at end of file

=== added file 'src/specs/KolibriContext.php'
--- src/specs/KolibriContext.php	1970-01-01 00:00:00 +0000
+++ src/specs/KolibriContext.php	2009-04-29 07:09:39 +0000
@@ -0,0 +1,138 @@
+<?php
+/**
+ * This class is the Kolibri Test framework. It serves as an class for Model, Action and View
+ * testing. Right now it support Action and Model testing. You have to extend KolibriContext
+ * to use this test-framework. It reflects the same methods as PHPSpec_Context has, but they
+ * are named differently. The corresponding method names are setup(), preSpec(), postSpec()
+ * and tearDown().
+ */
+class KolibriContext extends PHPSpec_Context {
+    public $fixtures;
+    public $modelName = null;
+    private $db = null;
+	private $testType;
+	
+	const ACTION_TEST = 'action';
+	const VIEW_TEST = 'view';
+	const MODEL_TEST = 'model';
+	
+    /**
+     * Executes before all spec methods are invoked. Distinguishes between model, action and view
+     * testing. It also establishes a database connection.
+     */
+    public function beforeAll () {
+		if (Config::getMode() != Config::TEST) {
+			throw new Exception("KolibriTestCase requires that the current KOLBRI_MODE is set to TEST.");
+		}
+		
+        $className = get_class($this);
+		
+        if (substr(strtolower($className), -5) == self::MODEL_TEST) {
+			$this->testType = self::MODEL_TEST;
+            $this->modelName = substr($className, 8, -5);
+			$this->fixtures = new Fixtures($this->modelName);
+        }
+        elseif (substr(strtolower($className), -6) == self::ACTION_TEST) {
+            $this->testType = self::ACTION_TEST;
+        }
+        elseif (substr(strtolower($className), -4) == self::VIEW_TEST) {
+			$this->testType = self::VIEW_TEST;
+            throw new Exception("KolibriContext does NOT support view testing yet.");
+        }
+        else {
+            throw new Exception("KolibriTestCase needs to have either Model, Action or View in the end of the classname");
+        }
+        
+		$this->db = DatabaseFactory::getConnection();
+		if (method_exists($this, 'setup')) {
+			$this->setup();
+		}
+    }
+	
+	/**
+	 * Triggers the preSpec() method for doing something _before_ a spec has been invoked. 
+	 */
+    public function before () {
+		$this->db->begin();
+		if (method_exists($this, 'preSpec')) {
+			$this->preSpec();
+		}
+    }
+
+	/**
+	 * Triggers the postSpec() method for doing something _after_ a spec. And it rolls back the current
+	 * changes in the database.
+	 */
+    public function after () {
+		$this->db->rollback();
+		if (method_exists($this, 'postSpec')) {
+			$this->postSpec();
+		}
+    }
+    
+	/**
+	 * Triggers the tearDown() method for doing something _after_ every spec has runned. 
+	 */
+    public function afterAll () {
+        unset($this->fixtures);
+        unset($this->modelName);
+		
+		if (method_exists($this, 'tearDown')) {
+			$this->tearDown();
+		}
+
+		unset($this->db);
+		
+		if (ob_get_level > 0) {
+			ob_flush();
+		}
+    }
+	
+	
+	/*
+	 * Methods for Action testing
+	 */
+	public function get ($uri, array $params = null, array $session = null) {
+		if ($this->validActionClass('get')) {
+			$this->prepareEnvironment('GET', $uri, $session);
+			$this->request = new Request($params !== null ? $params : array(), array());
+			$this->fireRequest($this->request);
+		}
+	}
+
+	public function post ($uri, array $params = null, array $session = null) {
+		if ($this->validActionClass('post')) {
+			$this->prepareEnvironment('POST', $uri, $session);
+			$this->request = new Request(array(), $params !== null ? $params : array());
+			$this->fireRequest($this->request);
+		}
+	}
+
+	private function fireRequest ($request) {
+		$rp = new RequestProcessor($request);
+		$this->response = $rp->process(false);
+		$this->action = $rp->getDispatcher()->getAction();
+	}
+
+	private function prepareEnvironment ($method, $uri, $session) {
+		$_SERVER['REQUEST_METHOD'] = $method;
+		$_SERVER['REQUEST_URI'] = $uri;
+		$_SESSION = $session !== null ? $session : array();
+	}
+	
+	/**
+	 * Does not allow you to use post and/or get in any other testing classes than action.
+	 *
+	 * @param string $method method that are tested for
+	 * @return bool returns true if its allowed to be used
+	 */
+	private function validActionClass ($method) {
+		if ($this->testType != self::ACTION_TEST){
+			throw new Exception("You are not allowed to use $method(), except in an action testing class.");
+		}
+		return true;
+	}
+
+}
+
+?>
\ No newline at end of file

=== added file 'src/specs/SpecHelper.php'
--- src/specs/SpecHelper.php	1970-01-01 00:00:00 +0000
+++ src/specs/SpecHelper.php	2009-04-28 06:38:28 +0000
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Loads all the correct files and mode for KolibriTestCase
+ *
+ * REMEMBER to require this file in every spec class you have
+ * <code>require_once(dirname(__FILE__) . '/../SpecHelper.php')</code>
+ * 
+ * Defines the root directory of the Kolibri framework. By default this is a directory named
+ * 'kolibri' within the document root.
+ */
+if (!defined('ROOT')) {
+	if (!$rootDir = getenv('KOLIBRI_ROOT')) {
+		throw new Exception('Environment variable KOLIBRI_ROOT must be defined.');
+		exit;
+	}
+	define('ROOT', $rootDir);
+
+	/*
+	 * Defines the root directory for the application. By default this is the same directory as
+	 * this kolibri.php file.
+	 */
+	$dirname = dirname(__FILE__);
+	if (basename($dirname) == 'specs') {
+		$path = dirname(__FILE__) . '/..';
+	}
+	else $path = dirname(__FILE__) . '/../..';
+
+	define('APP_PATH', $path);
+}
+
+putenv('KOLIBRI_MODE=test');
+require(ROOT . '/core/Config.php');
+require(ROOT . '/core/RequestProcessor.php');
+
+Config::getInstance();
+
+require(ROOT . '/specs/Fixtures.php');
+require(ROOT . '/specs/KolibriContext.php');
+
+$setupFile = APP_PATH . '/specs/setup.sql';
+$schemaFile = APP_PATH . '/config/schema.sql';
+
+if (file_exists($setupFile)) {
+	$db = DatabaseFactory::getConnection();
+	
+	if (file_exists($schemaFile)) {
+		$schemaContents = file_get_contents($schemaFile);
+		$db->batchQuery($schemaContents);
+		$db->commit();
+	}
+	
+	$setupContents = file_get_contents($setupFile);
+	$db->batchQuery($setupContents);
+	$db->commit();
+}
+?>

=== added file 'src/test.php'
--- src/test.php	1970-01-01 00:00:00 +0000
+++ src/test.php	2009-04-29 07:09:39 +0000
@@ -0,0 +1,30 @@
+<?php
+/**
+ * PHPSpec
+ * This file will run all specs within this directory and all child-directories.
+ * TODO safeguard sjekke at mode = test
+ */
+
+
+chdir('specs');
+
+require_once 'PHPSpec.php';
+
+/*
+ * We start the session here because the action tests are randomly executed
+ * and in some cases the session has not been initialized before the output.
+ */
+session_start();
+
+$options = new stdClass;
+$options->recursive = true;
+$options->specdoc = true;
+$options->reporter = 'html';
+
+ob_start();
+
+PHPSpec_Runner::run($options);
+
+ob_end_flush();
+
+?>
\ No newline at end of file


Follow ups