15 Jul 2010It seems that if you're creating a fixture for a model that uses an unconventional table name, the CakeTestFixturedoes not manage to correctly create the needed table when your test starts.
In fact, it will try to insert the fixture records into the right table (the one defined in $useTable), but won't have created the table beforehand.
In fact this is because it will correctly rely on the model $useTable property when inserting records but will try to guess the table name based on the model name when creating the table.
Anyway, the quick fix I've found so far is to define the $table property of your fixture to match your model $useTable.
class Client extends Model {
var $useTable = 'users';
}
class ClientFixture extends CakeTestFixture {
var $table= 'users';
}
I've filed a bug report as well as suggested a fix to allow cake to automatically use the correct table.
15 Jul 2010I needed to write a test that should fail if the method I was testing triggered a PHP E_NOTICE error (namely, an "undefined index" kind of error).
SimpleTest provides both expectError() and expectException() methods. I have only been able to make the former work with manually triggered errors (using trigger_error()), and the latter only catchs Exceptions.
After hours of googling, and reading SimpleTest source code, I finally managed to come up with a solution.
Redefining the error handler
It still amaze me how awfuly complicated it is to do such a simple thing. Anyway, the key is to redefine the error handler function so you can shortcircuit both PHP and SimpleTest on the way they managed errors.
Doing so you can detect when the code you test produce an error. Based on the $errno and $errstr supplied, you can even filter to exactly the error you're looking for.
Unfortunatly, you'll be blocked inside your newly defined error_handler function. It means you won't have access to the test, so won't be able to call $this->pass() nor $this->fail(), because $this does not reference anything.
Throw me out of here
The only trick I've found to get out of this function and back in the scope of your test is to throw an Exception. Namely, an ErrorException.
Doing so we can rewrote our test to put the part that is triggering the error inside a try/ catchstatement and manually pass or fail the test.
Don't you forget something ?
One very important thing not to forget here is to restore the error_handler to its original value once you won't need it. If you don't, you will surely broke all your subsequent tests because SimpleTest use its own error_handler too.
We have to restore it in two places, both when the code triggered and error / throw an exception and when it don't.
Cut the speech, show me the code
Okay, here's the code. Don't forget to adapt the condition in the error_handler to match your error.
function testGotUndefinedIndex() {
// Overriding the error handler
function errorHandlerCatchUndefinedIndex($errno, $errstr, $errfile, $errline ) {
// We are only interested in one kind of error
if ($errstr=='Undefined index: bar') {
//We throw an exception that will be catched in the test
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
return false;
}
set_error_handler("errorHandlerCatchUndefinedIndex");
try {
// triggering the error
$foo = array();
echo $foo['bar'];
} catch (ErrorException $e) {
// Very important : restoring the previous error handler
restore_error_handler();
// Manually asserting that the test fails
$this->fail();
return;
}
// Very important : restoring the previous error handler
restore_error_handler();
// Manually asserting that the test succeed
$this->pass();
}
13 Jul 2010For one of the tests I'm currently writing, I needed to assert that my method was correctly returning an error if the connection to the database was impossible.
I couldn't find an easy way to change datasource credentials once the app was initiated, so I decided to manually update the ConnectionManager inner values.
Here's how I did it :
// Getting the datasource cache in the ConnectionManager object
$connectionManagerInstance = ConnectionManager::getInstance();
$databaseConfig = &$connectionManagerInstance->_dataSources;
// Saving the initial setting for reverting it later
$_defaultConfig = $databaseConfig['default'];
// Changing the password so the credentials will fail
$databaseConfig['default']->config['password'].= 'pass';
// Getting the updated datasource
$connect = ConnectionManager::getDataSource('default');
// Error handling when connection unavailable
[...]
// And reverting the settings back
$databaseConfig['default'] = $_defaultConfig;
This proved really useful when testing to simulate a database server error.
05 Jul 2010The easiest way to display a date in a given format is to use a combination of strftime() and strtotime().
Sometimes, you also need your date to be displayed in a specific language. For example, for a french website, I needed a date to be displayed as "mardi 03 août 2010" instead of "Wednesday, August 3rd".
Setting the locale
To achieve that, you just have to tell PHP which locale to use when displaying date with setlocale(LC_TIME, $locale).
The value of $locale is OS dependent, though. For example, on a linux server, you have to set fr_FR while it is fr or even frenchon Windows.
Fortunatly, you can pass an array of locales to setlocale(, and the system will use the first one it can find. You just have to pass an array containing all possible values and you'll be good to go.
I wrote a little method that use the L10nobject that comes with cakePHP to automate the creation of such an array. Just feed him a 3-letter language code and it will return an array of the most common locale names.
function getLocales($lang) {
// Loading the L10n object
App::import('L10n');
$l10n = new L10n();
// Iso2 lang code
$iso2 = $l10n->map($lang);
$catalog = $l10n->catalog($lang);
$locales = array(
$iso2.'_'.strtoupper($iso2).'.'.strtoupper(str_replace('-', '', $catalog['charset'])), // fr_FR.UTF8
$iso2.'_'.strtoupper($iso2), // fr_FR
$catalog['locale'], // fre
$catalog['localeFallback'], // fre
$iso2 // fr
);
return $locales;
}
You may note that I set in first position a locale with the mention of the encoding. This is only used on Linux machines, Windows does not handle that. That's a pity, but I'll show you how to correctly make it work underWwindows.
As a side note, setlocale will not return false if the locale is not found, it will just fail to load it.
Displaying date in UTF8
If your app is in UTF8 (and it should be !) you may run into problem when trying to display a simple strftime("%B", strtotime($date)) on Windows.
%B translate to the current month name. For a month like Août (August) the funny û will not get correctly displayed, because Windows does its locale translation in its native encoding.
You'll need to manually encode it in utf8, but if you do so on a linux machine, as the result is already encoded in utf8 you may end in double encoding the same string, resulting in others display errors.
Note also that if your format string itself contains utf8 encoded characters (like%A %d %B %Y à %Hh%M), encoding it in utf8 again will also result in wrong characters displayed.
What I've done is creating a simple method in an helper that will take care of encoding the result if needed.
function time($format, $date = null) {
// On Windows, we will force the utf8 encoding of the date
if (DIRECTORY_SEPARATOR == '\\') {
return utf8_encode(strftime(utf8_decode($format), strtotime($date)));
}
// On linux, this is already taken care of by setlocale()
return strftime($format, strtotime($date));
}
This way, we make sure that the date is correctly displayed in utf8, no matter the OS, even if you already supply utf8 characters in the format string.
30 Jun 2010One pattern I use when writing new method is to put conditions that may end the script early on top. Like "stop the method if the arguments are not right" or "stop the action if the user is not loggued in".
It allow me to avoid having nested if/elsestatement that became unreadable.
Felix from Debuggable wrote something about that a while ago.
This is a pretty easy pattern to follow when writing methods, but can be quite harder to achieve if you need to stick inside a main method. You just want to go "out" of the if statement, to continue the script, but not end the method.
Break to the rescue
You can't use the breakkeyword in an if statement like you would in a loop. It just throws an error.
But, you can define a simple do {} while (false) loop and use the break goodness inside it.
do {
if (empty($data)) break;
$this->create($data);
if (!$this->validates()) break;
$this->save();
} while (false);
This helped me some times, hope it can help someone else.