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.