Thursday, July 15, 2010

Testing Symfony Apps With PHPUnit

[Alt-Title: Or It's NEVER as Easy As the Tutorials Show]

Symfony development cycle is done on a local machine, (probably many other frameworks also). This saves the long upload times, and allows a programmer/developer to work on a project even while on vacation without Internet . . . . uh, right.) Is is much faster. Another major benefit is not interfering with the production site . . . at all, even in subdomain, or a separate database.

Part of that cycle is to drop and recreate the database for every little change, especially when doing testing. It means a clean slate, no interference between tests.

Symfony2 will be swtiching from its own home brewed 'lime' testing framework to PHPUnit according to Dustin Whittle as of a presentation by him at a PHP Meetup 1 week ago. They WILL be extending it to recover what they will lose from lime, gaining both from in house efforts and the community efforts @ the PHP Unit group.

So even though I'm currently using Symfony 1.4.4, I thought I would start my transition to Doctrine 2 by using PHP Unit on my current project (the subject of this blog). Both Symfony 1.4.1 and PHP Unit are current products, and I thought that I wouldn't have to deal with 'first adopter' problems. Sigh, wishful thinking.

Let me preface by saying that I prefer and use Postgresql for the database. It may not have anything to do with the lesson learned and described here, but then maybe it does. I'll leave it for the audience to comment upon using their experiences.

So, running Ubuntu, Apache, Postgres, PHP, Symfony, and Doctrine, I built a moderately complex ERD/Schema. The first lesson?

(LESSON-1)
ALWAYS use substitute, BIG INT, primary keys. Why?
  • They are small compared to strings.
  • If the data changes, the indexes don't have to, (data as primary keys forces the database to recalculate indexes if the data changes.)
  • Symfony 1.x.x, and even Symfony 2/Doctrine 2 (according to a page at http://www.doctrine-project.com/ that I read) will not yet support a sequence with a muliti column, composite key. Just using a muliti column key would probably make the Doctrine ORM work harder and give it more chances to not do exactly what you had in mind

Using a substitute primary key, means that uniqueness on the DATA column potentially used as a primary key must then be enforced using an additional index on the/those column/s. So maybe the extra writing to the index still goes on. My bad. If you really want to learn about the 'Big Fight' surrounding substitute integer primary keys, use your favorite search engine to read about it. But it REALLY is better with web frameworks, trust me.

Having got past the Symfony/Doctrine framework not being able to create a model on composite keys involving a sequence, I have made great progress. In fact, I'm to the testing point on the main datagram/ERD/Schema in relationship to user input, searching, and output pages. Yehaww.

So, back to PHP Unit/Symfony. I set up an inheriting class of the main PHP Unit class 'PHPUnit_Framework_TestCase'. All methods that begin with 'test...' are executed when the class is executed by a CLI executable of PHPUnit, or when the execute() method of the class is called. They SEEM to be in the order that they are defined in the class, but I've only written two methods so far.

[LESSON-2]
Use exception test everywhere in PHPUnit derived class while doing PHPUnit/Symfony testing.
[LESSON-3]
Build fixtures (preset conditions) and tests for them INCREMENTALLY.

One reason is, the line numbers that PHPUnit feeds forward to the screen with errors is the line IT called, not where the error is. And the error stack is not that accurate either. And incremental building/testing (should be done together) lets you know, MOST of the time, where the error is.

Other reasons, inline echos come out of PHPUnit out of order from its message and its output. PHPUnit also seems to NOT output PHP errors, or stop at 'exit()', but to keep going past them. So it's better to build small pieces, build small steps,blah blah.

Did I do that at first? NO! That's why I can tell you it's easier doing so now :-)

NOW . . . The main lesson in this post.

[LESSON-4]
It's *NOT* a good idea to drop the database for every test. Reasons?

  • It SEEMS as if the make a database code in Postgres or Doctrine returns before it's actually done. In fact, as I read this, I seem to remember having read somewhere that dropping/creating the database is NOT transactionable. I can tell you from personal experience, that is SEEEEEEMS that way for me. I would get errors about columns not existing, or creating tables would fail, or foreign key violations, or other errors that didn't make sense. The datasets I'm using are tiny, and I KNOW that there's no chance of foreign key violations, and which columns exist or don't.
  • It takes a LONG time to drop a database and recreate one. Something like 7 seconds on my machine at home. If I am running 25-100 fixtures with several assertions each, that would be a long time, counting the data tests that will happen when the tests get much larger towards the end.
  • It takes a long time to ADD test data if it's any size at all, but this also affects my eventual solution - partially.
What do do otherwise? Delete the data in the tables, reset sequences/autoincrementing columns, reload data. The database stay sin existence. Several things have to be taken into account though.

Drop the tables in the order that it does not cause any foreign key violations, from lowest 'grandchild' table to the highest parent. IF your data is too complex for that, drop all the constraints, then recreate them before repopulating the test data. If you have large datasets, and your database supports it, TRUNCATE the tables. It essentially just erases the contents of the database files but without deleting the files. Finally, if your datasets are large, or you've run many tests on the database already, force your database to scan the tables and consolidate the records to the top of the table files in a smaller file. Different DBs call it different things. If you do TRUNCATE, you may not need to do the scan.

What did I try with Symfony/Doctrine/PHPUnit *BEFORE* resorting to doing something more conventinal/pre framework era-like:

1/ Before each PHPUnit test, I ran the command line reset of the project using one of PHP's host command line invocation functions, exec(). I did it like this:

Executing it from outside of PHP while inside of the Program.
  //the following was an attempt to let Postgres drop a database.

// Postgres will not do so while there are connections to a database.
Doctrine_Manager::getInstance()->getCurrentConnection()->close();

exec('./symfony doctrine:build --all --and-load --no-confirmation');

This drops the database, recreates it, recreates the tables, then the constraints, then loads the data, and does not ask for confirmtatio of anything. Using that on the command line (not from within a program) is the normal command line, user land approach. This did NOT avoid any of the timing issues ,and always resulted with strange errors.

1/ Before each PHPUnit test, I ran the Symfony task from WITHIN symfony by doing this:

using a Task from inside of symfony:
  $optionsArray=array();

$argumentsArray=array();

$optionsArray[]="--all";

$optionsArray[]="--and-load";

$optionsArray[]="--no-confirmation";

$task = new sfDoctrineBuildTask($configuration->getEventDispatcher(),

new sfFormatter());

$task->run($argumentsArray, $optionsArray);


This had slightly different errors . . . . sometimes.


SOOOOOOOooooooo, since I've been in construction, I less frequently site there and try and figure out what the right thing is supposed to do, or what the designers were really trying to get us to do. I JUST GET IT WORKING. I knew that doing it the straight SQL way would work, so that's what I did.


[LESSON-5]Doctrine's 'rawSqlblah' functions aren't really 'raw' or that transparent.


BUT, I did find a post that showed how to do it, symfony framework forum: General discussion => [HOWTO] True RAW SQL in Doctrine. The thing is to avoid mixing PDO, or even lower level code withi Symfony/Docrine code because you can REALLY screw up the multi level transaction code inside of Doctrine. And, it STILL is simpler, even using raw SQL vs ORM, to use the Doctrine ORM for the connection. Your code has to be database specific in some cases, though. Here's how to do it for Postgres:

//multiple statements not possible in prepared queries (used by default)

$sql=array();

// removes every record,if the constraints allow

$sql[]="delete from table_one";

//sets it back to starting value, usually 1.

$sql[]="alter sequence table_one_id_seq restart";

// removes every record,if the constraints allow

$sql[]="delete from table_two";

//sets it back to starting value, usually 1.

$sql[]="alter sequence table_two_id_seq restart";

$doctrine = Doctrine_Manager::getInstance()->getCurrentConnection()->getDbh();

foreach( $sql as $do ){
$doctrine->query($do);
}

//Still possible, and EASIER to load the (default) fixture file using external commands:

//Symfony command line commands must always be done from project root

chdir(sfConfig::get('sf_root_dir'));
exec('./symfony doctrine:data-load');

So, this post talked about 5 lessons about using Symfony with PHPUnit that YOU don't have to learn the hard way if you don't want to:

[Alt-Title: Or It's NEVER as Easy As the Tutorials Show]
(LESSON-1)
ALWAYS use substitute, BIG INT, primary keys. Why?
[LESSON-2]
Use exception test everywhere in PHPUnit derived class while doing PHPUnit/Symfony testing.
[LESSON-3]
Build fixtures (preset conditions) and tests for them INCREMENTALLY.
[LESSON-4]
It's *NOT* a good idea to drop the database for every test.
[LESSON-5]
Doctrine's 'rawSqlblah' functions aren't really 'raw' or that transparent. See this link about the subject and better way to do it: symfony framework forum: General discussion => [HOWTO] True RAW SQL in Doctrine


Site references:
http://www.phpunit.de/
http://www.symfony-project.com/
http://www.doctrine-project.com/

[NOTES]
1/ So why was I trying to use a sequence/autoincrementing column in combination with the primary key columns? There were/are 8 integer columns that are fed from 4 other tables that need to be unique. To use THOSE as the foreign keys in the the table that is a child of that table, I really needed to make those have mulitple occurrences in that child table. BUT I wanted to be able to search for it easier and have LOTS easier database code to write, especially if I eventually go to C++ for somethings for speed. (Please, it does happen :-) There's more to it that that, proprietary to the site, that I can't divulge.

No comments:

Post a Comment