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
sarimarton2008-02-07 23:02:57
Hey, thanks for this loader, it really helped me loading the fixtures on the 'test' connection.
However, the cookie method doesn't work for me because the bootstrap runs first before a customWebTest class, therefore the information that we're testing is not present that time.
So I simply check the url to get the information whether i'm in a test or not:
if (pregmatch('/\/test\.php$/', $SERVER'PHP_SELF')) {
define('USEDB', 'test');
} else {
define('USEDB', 'default');
}
Then in my AppModel I use:
var $useDbConfig = USE_DB;
Matt2008-02-08 08:15:43
@sarimarton:
Thanks for your feedback.
Maybe I didn't explained myself, but the intention of this approach is to make the requested pages to use the test database you have created.
E.g.
You go to test.php and run a test depending of this CustomWebTestCase.
When you run the class, the cookie is not set, so it uses the database source that you have especified in your test models using $useDbConfig.
When the test is run, it sets the cookie and then request a web page for example /accounts/login/, in that case, the Cookie is set so the bootstrap of the requested page sets the test database.
That way you achieve two things when web testing: you don't alter your development data and you can make use of fixtures.