Test-driven Drupal development
Drupal's implementation of SimpleTest-style testing is an amazing tool for developing complex applications. When code is complex enough, a simple change can break things in unexpected places, but with good test coverage, this breakage can quickly be discovered and fixed. The same goes for deploying Drupal sites. By default SimpleTest creates a parallel test database from the ground up, meaning that it installs a brand new Drupal site, rather than copy the instance of Drupal it is installed on. This is all well and good for module development, and maintaining a solid core, but when it comes to testing complex site configurations, it would be nice to be able to test that all the installed modules, configured in such and such a way, are all playing nicely together. Taking this one step further, for a really complex site, it becomes possible to write a set of acceptance tests before site configuration even starts. Knowing that these tests must eventually pass can significantly focus and drive the development work.
That's where overriding the DrupalWebTestCase setUp() function comes into play. SimpleTest will run the setUp() function as defined in the DrupalWebTestCase() class unless the current test case has an overriding setUp() function. Some existing tests append additional functionality during set up, but most usually call the parent method, which is responsible for installing that fresh instance of Drupal.
To copy the existing set up, the parent function can be overridden on a per-test-case basis:
class MyModuleHelperTestCase extends DrupalWebTestCase { /** * Implementation of setUp(). */ function setUp() { // Here the existing Drupal instance will be cloned. }
By defining the function here, the parent method won't be called by SimpleTest. Also note that this helper test case can be called by additional test cases so that the cloning of the database doesn't need to be re-coded for each test case. In order to clone the existing set up, the database schema is needed. Once we have the schema, it is looped through, creating identical tables, and then populating them:
/** * Implementation of setUp(). */ function setUp() { // Don't create test db via Drupal install method, instead copy existing db. global $db_prefix; // Store necessary current values before switching to prefixed database. $this->db_prefix_original = $db_prefix; // Get schema of existing database. $schemas = drupal_get_schema(); // Generate prefixed database so tests don't interfere with live database. $this->db_prefix_test = $db_prefix = 'simpletest' . mt_rand(1000, 1000000); // Copy each table into new database. foreach ($schemas as $name => $schema) { $this->cloneTable($name, $schema); } // NOTE: Everything below here is the same as parent::setUp(). // Rebuild caches. menu_rebuild(); actions_synchronize(); _drupal_flush_css_js(); $this->refreshVariables(); // Use temporary files directory with the same prefix as database. $this->original_file_directory = file_directory_path(); variable_set('file_directory_path', file_directory_path() . '/' . $db_prefix); file_check_directory(file_directory_path(), TRUE); // Create the files directory. }
The key piece of code in there is the call to the function cloneTables(). This creates the identical table, with the testing prefix, and then copies all the data from the live database, to the test table. Note that due to some odd behavior in mysql (in that it disallows inserting of a 0 value into a primary key field), special handling is needed for the users table.
/** * Mirror over an existing tables structure, and copy the data. * * @param $name * Table name. * @param $schema * A Schema API definition array. * @return * Array of table creation results. */ function cloneTable($name, $schema) { $return = array(); db_create_table($return, $name, $schema); if ($name == 'users') { // UID = 0 confuses mysql. Special handling here, taken from system.install db_query("INSERT INTO %s SELECT uid +1, name, pass, mail, mode, sort, threshold, theme, signature, created, access, login, status, timezone, language, picture, init, data FROM %s", $this->db_prefix_test . $name, $this->db_prefix_original . $name); // Update uid db_query("UPDATE %s SET uid = uid - 1", $this->db_prefix_test . $name); } else { // Copy over data (brackets are /not/ used or this wouldn't work). db_query("INSERT INTO %s SELECT * FROM %s", array($this->db_prefix_test . $name, $this->db_prefix_original . $name)); } }
That's it! At this point, we have a test database that is functionally identical to our existing database, so we can hack away at it with the built in testing browser, and make sure everything is behaving as expected. In order to not duplicate the above set up function for every test case we write, additional test cases extend the helper test case:
class MyModuleFooBarTestCase extends MyModuleHelperTestCase { ... function testSomePathBehavior() { $this->drupalGet('some/path'); $this->assertRaw('<div id="my-expected-id">foobar</div>', t('Found foobar on some/path')); ... } }
For more information on writing acceptance tests and unit tests for Drupal, check out the handbook pages.

Comments
The auto value during the
The auto value during the user table import could also be suppressed by issuing the following command (taken from here):
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";Except, of course, you absolutely do not want vendor-specific SQL in your code.
nice post
this sort of thing will become very valuable as testing moves beyond drupal-core and the rest of the community gets serious about it. thanks for the tip!
Post new comment