gignus.com

Web development

Welcome to gignus.com, this are my latests post.

Testing with CakePHP 1.2

Created: 2008-01-26 18:12:09, last updated: 2008-02-07 22:47:56

Update:I have updated some part of this article, please read Cake Unit Testing: Dropping and recreating tables on each test takes too much time after reading this post.

I've started a big project recently (can't talk about because I signed an NDA) and I'm trying to use as much agile practices as I can to keep the project and the code on good health.

Up to know I'm very happy because I've been able to test drive all the code. Some things slowed me down at first, but now that the base is set up I'm feeling more productive. Also, I think that having the confidence to refactor the code is price-less.

If you're like me, you don't like to be slowed down, so in this post I will talk about one of my experiences while testing with CakePHP.

My setup

I've found controller testing to be very hard to execute. To be able to test my controller branches, I would have to Mock up models, components and view. To do that, and I would have to modify the dispatcher to let me call the mocked objects and start mocking stuff. I didn't had the mood to do all that, so instead of testing controllers I created integration tests (a.k.a. Web tests) to test my controller branches.

Setting up

The first thing I noticed is that when you are web testing an application, it is agnostic of whenever you're a user or a testing script, so it doesn't use the testing database.

I'm not much comfortable with my approach, but couldn't think of a better one at the moment.

What I do, is to create a cookie before each web test and check the value of that cookie in my bootstrap to set up test-dependent constants. I put this logic in a new CustomCakeWebTestCase that extends CakeWebTestCase in the app's vendors directory. I also wanted to create, populate and delete my tables on each test, so I copied the functionality of the CakeTestCase into a class named FixturesLoader and invoke it from my CustomCakeWebTestCase.

app/vendors/custom_web_test_case.php


vendor('fixtures_loader');

class CustomCakeWebTestCase extends  CakeWebTestCase  {
	function setUp() {
		$this->setCookie('TEST_MODE', true );
                $this->fixturesLoader = new FixturesLoader( $this->fixtures );
		$this->fixturesLoader->start();
	}

	function tearDown() {
		$this->fixturesLoader->end();
		$this->setCookie('TEST_MODE', false );
	}

}

app/config/bootstrap.php

if( isset($_COOKIE['TEST_MODE']) &&  $_COOKIE['TEST_MODE'] ) { 
	define('DB_PREFIX', 'test_case_');
} else {
	define('DB_PREFIX', '');
}

app/config/database.php

class DATABASE_CONFIG {

	var $default = array(
		'driver' => 'mysql',
		'persistent' => false,
		'host' => 'localhost',
		'port' => '',
		'login' => 'root',
		'password' => '*********',
		'database' => 'mydatabase',
		'schema' => '',
		'prefix' => DB_PREFIX,
		'encoding' => ''
	);


	var $test = array(
		'driver' => 'mysql',
		'persistent' => false,
		'host' => 'localhost',
		'port' => '',
		'login' => 'root',
		'password' => '*********',
		'database' => 'mydatabase',
		'schema' => '',
		'prefix' => 'test_case_',
		'encoding' => ''	
	);
	
}

app/vendors/fixture_loader.php


class FixturesLoader {
	private $fixtures;
	var $__truncated = true;
	var $autoFixtures = true;
	var $_fixtureClassMap = array();
	
	function __construct( $fixtures = null ) { 
		$this->fixtures = $fixtures;	
	}
	
	function start()  {
		$this->cleanFixtures();
		 
		// Set up DB connection
		if (isset($this->fixtures)) {
			$this->_initDb();
			$this->_loadFixtures();
		}

		if (isset($this->_fixtures) && isset($this->db)) {
			foreach ($this->_fixtures as $fixture) {
				$fixture->create($this->db);
			}
		}
		
		// Create records
		if (isset($this->_fixtures) && isset($this->db) && $this->__truncated && $this->autoFixtures == true) {
			foreach ($this->_fixtures as $fixture) {
				$inserts = $fixture->insert($this->db);
			}
		}
	}

	function end() {
		if (isset($this->_fixtures) && isset($this->db)) {
			foreach (array_reverse($this->_fixtures) as $fixture) {
				$fixture->drop($this->db);
			}
		}
	}

	function cleanFixtures() { 
		if (isset($this->fixtures) && (!is_array($this->fixtures) || empty($this->fixtures))) {
			unset($this->fixtures);
		}		
	}	
	
	function _initDb()  {
		$testDbAvailable = false;

		if (class_exists('DATABASE_CONFIG')) {
			$dbConfig =& new DATABASE_CONFIG();
			$testDbAvailable = isset($dbConfig->test);
		}

		if ($testDbAvailable) {
			// Try for test DB
			restore_error_handler();
			@$db =& ConnectionManager::getDataSource('test');
			set_error_handler('simpleTestErrorHandler');

			$testDbAvailable = $db->isConnected();
		}

		// Try for default DB
		if (!$testDbAvailable) {
			$db =& ConnectionManager::getDataSource('default');
			$db->config['prefix'] = 'test_suite_';
		}

		ConnectionManager::create('test_suite', $db->config);
		// Get db connection
		$this->db =& ConnectionManager::getDataSource('test_suite');
		$this->db->cacheSources  = false;
		$this->db->fullDebug = false;		
	}
	
/**
 * Load fixtures specified in var $fixtures.
 *
 * @access private
 */
	function _loadFixtures() {
		if (!isset($this->fixtures) || empty($this->fixtures)) {
			return;
		}

		if (!is_array($this->fixtures)) {
			$this->fixtures = array_map('trim', explode(',', $this->fixtures));
		}

		$this->_fixtures = array();

		foreach ($this->fixtures as $index => $fixture) {
			$fixtureFile = null;

			if (strpos($fixture, 'core.') === 0) {
				$fixture = substr($fixture, strlen('core.'));
				foreach (Configure::corePaths('cake') as $key => $path) {
					$fixturePaths[] = $path . DS . 'tests' . DS . 'fixtures';
				}
			} elseif (strpos($fixture, 'app.') === 0) {
				$fixture = substr($fixture, strlen('app.'));
				$fixturePaths = array(
					TESTS . DS . 'fixtures',
					VENDORS . 'tests' . DS . 'fixtures'
				);
			} else {
				$fixturePaths = array(
					TESTS . 'fixtures',
					VENDORS . 'tests' . DS . 'fixtures',
					TEST_CAKE_CORE_INCLUDE_PATH . DS . 'cake' . DS . 'tests' . DS . 'fixtures'
				);
			}

			foreach ($fixturePaths as $path) {
				if (is_readable($path . DS . $fixture . '_fixture.php')) {
					$fixtureFile = $path . DS . $fixture . '_fixture.php';
					break;
				}
			}

			if (isset($fixtureFile)) {
				require_once($fixtureFile);
				$fixtureClass = Inflector::camelize($fixture) . 'Fixture';
				$this->_fixtures[$this->fixtures[$index]] =& new $fixtureClass($this->db);
				$this->_fixtureClassMap[Inflector::camelize($fixture)] = $this->fixtures[$index];
			}
		}

		if (empty($this->_fixtures)) {
			unset($this->_fixtures);
		}
	}	
}

Now you just have to include your CustomWebTestCase in your test files, for example:

app/test/cases/integration/products.test.php

vendor('custom_cake_web_case.class');

class ProductsIntegrationTest extends CustomCakeWebTestCase {	
	public $fixtures = array('app.product');

	function testSearchWithBlankCriteriaFails() { 
		//the code goes here
	}
}

I hope you find this information useful, if you have another approach to testing, please let me know.

You can find more information about web testing with simpletest here and here.

2 comments

Code highlighting helper for cakephp

Created: 2008-01-10 22:34:34, last updated: 2008-01-15 17:42:17

I've recently build a simple code highlighter helper for cakephp using GeSHi and PHP5 native DOM parser.

I just wanted something to highlight my code, and I want it fast, so the code is a little hacky. I'm open to your suggestions on cleaning up, fixes or enhancements.

Requirements

Instalation

  1. Download GeSHi and place it in your app/vendors/geshi/ folder.
  2. Edit the app/vendors/geshi/classes/renderers/class.geshirendererhtml.php and change the getHeader and getFooter to return nothing.

    class GeSHiRendererHTML extends GeSHiRenderer
    {
        //stuff
    
        function getHeader ()
        {
            //return '<pre style="background-color:#ffc;border:1px solid #cc9;">';
            return '';
        }
    
        function getFooter ()
        {
            //return '</pre>';
            return '';
        }
    }
        
  3. Place this helper code in app/views/helpers/code.php

Use

The usage of this helper is very simple. You wrap the code you want to highlight with <pre class="code {language}"></pre> where {language} is the language of the highlighted code. For example, when I want to post some php code in this blog, I just wrap it with <pre class="code php"></pre>.

Then I pass all the code to this helper highlight method in my view:

echo $code->highlight($post['Post']['body']); 

The supported languages are all those supported by GeSHi (you can find the list on GeSHi homepage). To enable them, you have to add them to the $validLanguages attribute of CodeHelper class:

class CodeHelper extends AppHelper {
	public $validContainers = array( 'pre' ); 
	public $validLanguages = array( 'php', 'javascript', 'c' );

        //more stuff

To-do/Known bugs

Remove the changes in the GeSHI code to eliminate the default container

Clean up the code

Having to escape entities twice for them to appear encoded on the output.

Download

5 comments

Custom pagination finder in cakephp 1.2

Created: 2008-01-10 10:27:01, last updated: 2008-01-10 10:32:29

Yesterday, while talking about Cake's pagination with Gnuget, we found that before calling findAll with our conditions, the paginate() method of the controller checks if a method called paginate exists in the paginated model.

if (method_exists($object, 'paginate')) {
     $results = $object->paginate($conditions, $fields, $order, $limit, $page, $recursive);
} else {
     $results = $object->findAll($conditions, $fields, $order, $limit, $page, $recursive);
}
This means that we could create a custom method in our model that receives the following parameters:
function paginate($conditions, $fields, $order, $limit, $page, $recursive) {
     // Our code goes here
}

We must take into account that we should manually filter the results in our custom query by using $limit and $page.

Other useful pagination resources:

0 comments

Copyright © 2007 Matias Lespiau - Powered by CakePHP