Unit tests with CakePHP

<!-- google_ad_section_start -->

I’ve spent a large part of yesterday setting up the testing environment for a CakePHP project.  As always, every time I do something that I have done before, I wanted to do it better, using all the experienced that was acquired previously.  And this often leads to the discovery of new things – both good and bad.  Here is a record of what I’ve learned yesterday.

Memory limit

Running ‘All Tests’, I kept getting an out of memory error, which was weird.  I increased the memory_limit in the global PHP configuration file (/etc/php.ini), but the number of allocated bytes mentioned in the error message wasn’t changing.  No matter how many times I restarted my web server.  It turns out, CakePHP allows running tests with a different memory_limit value than the rest of your application.   You can change this value in app/webroot/test.php file.

I consider this to be a very useful option.  In some cases, unit tests might require much more memory than the actual application.  In other cases, you’d want to limit tests’ memory to see if anything weird is going on, while providing more memory for the application itself.  Having an option to control this behaviour is A Good Thing.

Fixtures with real data

Previously I only used ‘manual’ fixtures, where I was defining by hand the structure of the tables and data that goes into them – all inside a fixture.  For my current project however it makes more sense to use data from the real database.  One of the reasons is because we’ll probably end up using Soft Deletable Behaviour for all models, which will preserve all the records in the database.

Setting up CakePHP fixtures to automatically import data from a real database turned out to be trivial.  All as described in Preparing test data chapter of The Book.  Except for two little things.

  1. If you have tables with a lot of records, CakePHP will probably run out of memory when copying data to the test table.  Point the fixture to the model in the $import array, but don’t set ‘records’=>true.  Better define records manually in the fixture.
  2. If you have any timestamp fields in your schema, you’ll probably discover that there is a bug in CakePHP that will prevent your fixtures from creating test tables.  See below for more details.

MySQL schema generator bug

When fixtures were prepared and tests written and I was hoping all will run smoothly, I encountered a weird problem – a MySQL error.  Here is what was happening:

  1. Test suite was loading fixtures.
  2. It saw that it had to create a table test_suite_something for model Something.
  3. It inspected the model Something to find which table it was using.
  4. It was getting table something schema from MySQL (explain something).
  5. It was generating an SQL query to create an identical table with a different name (test_suite_something)
  6. It was failing to run that SQL query due to an error in that query.

After digging deeper into CakePHP sources, I found where the problem was.  In file cake/libs/model/datasources/dbo_source.php there is a method buildColumn (around line 2346).  Here is the relevant part of that method:


} elseif (isset($column['default']) && isset($column['null']) && $column['null'] == false) {
$out .= ' DEFAULT ' . $this->value($column['default'], $type) . ' NOT NULL';
 } elseif (isset($column['default'])) {
$out .= ' DEFAULT ' . $this->value($column['default'], $type);
 } elseif (isset($column['null']) && $column['null'] == true) {
$out .= ' DEFAULT NULL';
 } elseif (isset($column['null']) && $column['null'] == false) {
$out .= ' NOT NULL';
 }

The problem here is that if you have, for example, a field of type timestamp with NOT NULL constraint, MySQL will also set this field’s default value to CURRENT_TIMESTAMP.  And CakePHP will get it from the table schema description.  However, when it will generate the SQL query for CREATE TABLE test_suite_something, according to the code above, it will single quote the default value of CURRENT_TIMESTAMP.  Here is how the query will look:


CREATE TABLE `something` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(255) NOT NULL,
 `parent_id` int(11) DEFAULT 0 NOT NULL,
 `created` datetime DEFAULT NULL,
 `modified` timestamp DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,    PRIMARY KEY  (`id`));

This query will generate error if you will send it to MySQL.  And even if it worked, it would be incorrect, because CURRENT_TIMESTAMP should not be in single quotes – it’s a built-in MySQL value, not a generic string.

Once the problem is known, the solution is usually pretty easy to do.  I decided not to patch the method in dbo_source.php, which would require me to modify an existing method, complicate upgrades, and probably break compatibility with non-MySQL databases.  Instead, a more elegant solution seemed to be adding a buildColumn() method to cake/libs/model/datasources/dbo/dbo_mysql.php .  Here is the code for the method I added:


/**
 * Generate a database-native column schema string
 *
 * All the hard work is done by the parent method. We just fix a few minor things for MySQL
 *
 * @param array $column An array structured like the following: array('name'=>'value', 'type'=>'value'[, options]),
 *   where options can be 'default', 'length', or 'key'.
 * @return string
 */
 function buildColumn($column) {
$result = parent::buildColumn($column);

$unquotes = array('CURRENT_TIMESTAMP', 'NULL', 'NOT NULL');
foreach ($unquotes as $unquote) {
$result = preg_replace("/DEFAULT\s'{$unquote}'/", "DEFAULT {$unquote}", $result);
}

return $result;
 }

This way we call the parent::buildColumn() method and then just fix the results for MySQL.

Once I figured out that last bit – everything just started working like a charm.  Fixtures were importing test data, unit tests were executing, and I got a confidence boost for upcoming code changes.

P.S.: All of the above is based on CakePHP 1.2.6 version.

<!-- google_ad_section_end -->