Pat Shaughnessy

Ribadesella, Spain

Using transactions in a separate database with Drupal PHPUnit tests

January 19, 2009 · 3 comments

Update April 2009:
I’ve started working with Mark Bennett on phpunit_setup.inc and moved it up to a github repository. Mark has adapted it to work with Drupal 5, and we’re working on a number of other related ideas as well. I’ll post updates here about our progress.

This month I’ve been experimenting with using testing ideas from Ruby on Rails while developing a Drupal module. To read more, see:

If you want to try this out yourself, follow these instructions:

  1. Edit settings.php and use an array of two values for $db_url:

    $db_url["default"] = 'mysql://user:password@localhost/drupal;
    $db_url["test"] = 'mysql://user:password@localhost/drupal_test';
  2. Create a new test database in MySQL:

    CREATE DATABASE drupal_test DEFAULT CHARACTER SET utf8
                                COLLATE utf8_unicode_ci;
  3. Download and save phpunit_setup.inc somewhere in your Drupal application; for example in the “includes” folder.
  4. Include phpunit_setup.inc at the top of each of your PHPUnit test classes. See one of the two articles above for example PHPUnit tests.
  5. Execute your PHPUnit test class from the root folder of your Drupal app:

    $ cd /path/to/your/drupal-site
    $ phpunit YourClass modules/your_module/YourClassFileName.php 
    PHPUnit 3.2.21 by Sebastian Bergmann.
    ..
    Time: 0 seconds
    OK (2 tests)

For more information about how to run PHPUnit with Drupal, see: Writing your first PHPUnit test in Drupal.

3 comments Tags:

Using MySQL transactions with Drupal unit tests

January 19, 2009 · 2 comments

Last time I wrote about how to use an entirely separate MySQL database to hold test data for Drupal unit tests, similar to the approach that the Ruby on Rails framework uses for test data. In this post I’ll look at another Rails innovation that can be applied equally well to unit testing with Drupal: running each unit test in a separate database transaction.

First let’s take a quick look at what database transactions are, and how we would use them while running unit tests. In a nutshell, a database transaction is just a way to group a series of SQL operations together and insuring that they are all run together as a single unit – either all of them are executed, or none of them. Let’s take an example. Here are some of the SQL statements Drupal executes when you save a new node in the database:

BEGIN
INSERT INTO node_revisions (nid, uid, title, body, teaser, log, timestamp...
INSERT INTO node (vid, type, language, title, uid, status, created, changed...
UPDATE node_revisions SET nid = 2 WHERE vid = 2
COMMIT

Normally Drupal does not use transactions, but I've inserted the “BEGIN” and “COMMIT” commands here as an example: the transaction starts with the BEGIN command, and ends with the COMMIT command. When MySQL receives the COMMIT command, it allows other database clients (future Drupal HTTP requests, or possibly future command line unit tests) to see the new inserted and updated node data. However, if the transaction were rolled back like this:

BEGIN
INSERT INTO node_revisions (nid, uid, title, body, teaser, log, timestamp...
INSERT INTO node (vid, type, language, title, uid, status, created, changed...
UPDATE node_revisions SET nid = 2 WHERE vid = 2
ROLLBACK

… then none of the changes would be made to the node and node_revisions tables. Instead when MySQL receives the ROLLBACK command it will discard the changes and these tables will appear the same way they did before the transaction started. Therefore, by running each unit test in a separate transaction and rolling it back at the end of each test, we can insure that any changes made to the database by that test are discarded… before the next test is run. Below I’ll explain how to actually do this with PHPUnit and Drupal.

But first let me quickly mention another huge benefit to using transactions with unit test suites: test performance. To learn more about why your tests will run a lot faster using MySQL transactions, read this great article by Mike Clark from the period when Rails 1.0 was released, way back in 2005. What Mike wrote about Rails in 2005 is still true today for Drupal: your unit tests will run faster because fewer SQL statements are required. You won’t need to execute DELETE SQL statements to remove the data after each test since rolling back each transaction accomplishes the same thing.

Now let’s get it to work with Drupal… first let me add a second unit test to my simple PHPUnit test class from last time:

<?php
require_once './includes/phpunit_setup.inc';
class TestDataExampleTest2 extends PHPUnit_Framework_TestCase
{  
  public function create_test_blog_post()
  {
    $node = new stdClass();
    $node->title = "This is a blog post";
    $node->body = "This is the body of the post";
    $node->type = "Story";
    $node->promote = 1;
    node_save($node);
    return $node;
  }
  public function test_there_is_one_post()
  {
    $this->create_test_blog_post();
    $this->assertEquals(1, db_result(db_query("SELECT COUNT(*) FROM {NODE}")));
  }
  public function test_there_are_two_posts()
  {
    $this->create_test_blog_post();
    $this->create_test_blog_post();
    $this->assertEquals(2, db_result(db_query("SELECT COUNT(*) FROM {NODE}")));
  }
}
?>

In this example, I use the phpunit_setup.inc file I wrote in my last post to clear out and setup a new Drupal schema each time I run the tests. Even though I have a clean test database each time I run PHPUnit, without using transactions one of these two unit tests will fail since each one creates its own test data, and assumes no other test data exist in the node table:

$ phpunit TestDataExampleTest2
          modules/test_data_module/TestDataExampleTest2.php 
PHPUnit 3.2.21 by Sebastian Bergmann.
.F
Time: 0 seconds
There was 1 failure:
1) test_there_are_two_posts(TestDataExampleTest2)
Failed asserting that <string:3> matches expected value <integer:2>.
/Users/pat/htdocs/drupal4/modules/test_data_module/TestDataExampleTest2.php:24
FAILURES!
Tests: 2, Failures: 1.

Here the second test fails since the blog post created in the first test is still present in the database. The simplest way to start a new database transaction before each test is run, and to rollback after each test is completed, is with the PHPUnit setup/teardown methods as follows:

public function setup()
{
  db_query("BEGIN");
}
public function teardown()
{
  db_query("ROLLBACK");
}

If you add these functions to the “TestDataExampleTest2” class above both tests should now pass since the ROLLBACK call will delete the nodes created by each test each time teardown is called…

$ phpunit TestDataExampleTest2
          modules/test_data_module/TestDataExampleTest2.php 
PHPUnit 3.2.21 by Sebastian Bergmann.
.F
…
FAILURES!
Tests: 2, Failures: 1.

Wait… what happened? It failed!

The problem is that MySQL does not support transactions using the MyISAM database engine, which is what Drupal uses by default. What we need to do is to convert all of the Drupal MySQL tables to use the InnoDB database engine instead. Unfortunately, there are many implications to using InnoDB vs. MyISAM in Drupal or with any MySQL based application. See "MySQL InnoDB: performance gains as well as some pitfalls" to read more. Specifically, there can be performance issues and degradation when using InnoDB incorrectly, or depending on the type of application you have. Drupal was actually designed and developed with MyISAM in mind, and not InnoDB, although there is some chance this might change for Drupal 7 someday.

Despite all of this, using InnoDB in a test database is a great idea since you will get all of the benefits of isolating tests from each other without having to worry about how InnoDB will effect your production site’s performance. In fact, the performance of your tests will actually be dramatically improved, as Mike Clark explained.

With all of this in mind, I wrote some code to convert the newly created Drupal tables in the test database from MyISAM to InnoDB right after we clear out and reload the test database. Here’s how it works; this code is from phpunit_setup.inc, which I included at the top of my PHPUnit test file:

function enable_mysql_transactions()
{
  convert_test_tables_to_innodb();
  db_query("SET AUTOCOMMIT = 0");  
}
function convert_test_tables_to_innodb()
{
  each_table('convert_to_innodb');  
} 
function each_table($table_callback)
{
  global $db_url;
  $url = parse_url($db_url['test']);
  $database = substr($url['path'], 1);
  $result = db_query("SELECT table_name FROM information_schema.tables
                      WHERE table_schema = '$database'");
  while ($table = db_result($result)) {
    $table_callback($table);
  }
}
function convert_to_innodb($table)
{
  db_query("ALTER TABLE $table ENGINE = INNODB");
}

This iterates over the Drupal tables in the test database and executes ALTER TABLE … ENGINE = INNODB on each one. The SET AUTOCOMMIT=0 command is used to prevent SQL statements from being committed immediately after they are executed, and to allow the InnoDB transactions to work properly.

To repeat and summarize how to employ and separate MySQL test database and transactions in your PHPUnit tests for Drupal, just follow these steps:

  1. Edit settings.php and use an array of two values for $db_url:

    $db_url["default"] = 'mysql://user:password@localhost/drupal;
    $db_url["test"] = 'mysql://user:password@localhost/drupal_test';
  2. Create a new test database in MySQL:

    CREATE DATABASE drupal_test DEFAULT CHARACTER SET utf8
                                COLLATE utf8_unicode_ci;
  3. Download and save phpunit_setup.inc somewhere in your Drupal application; for example in the “includes” folder.
  4. Include phpunit_setup.inc at the top of each of your PHPUnit test classes.
  5. Execute your PHPUnit test class from the root folder of your Drupal app:

    $ cd /path/to/your/drupal-site
    $ phpunit YourClass modules/your_module/YourClassFileName.php 
    PHPUnit 3.2.21 by Sebastian Bergmann.
    ..
    Time: 0 seconds
    OK (2 tests)

Here’s my finished test class:

<?php
require_once './includes/phpunit_setup.inc';
class TestDataExampleTest2 extends PHPUnit_Framework_TestCase
{  
  public function setup()
  {
    db_query("BEGIN");
  }
  public function teardown()
  {
    db_query("ROLLBACK");
  }
  public function create_test_blog_post()
  {
    $node = new stdClass();
    $node->title = "This is a blog post";
    $node->body = "This is the body of the post";
    $node->type = "Story";
    $node->promote = 1;
    node_save($node);
    return $node;
  }
  public function test_there_is_one_post()
  {
    $this->create_test_blog_post();
    $this->assertEquals(1, db_result(db_query("SELECT COUNT(*) FROM {NODE}")));
  }
  public function test_there_are_two_posts()
  {
    $this->create_test_blog_post();
    $this->create_test_blog_post();
    $this->assertEquals(2, db_result(db_query("SELECT COUNT(*) FROM {NODE}")));
  }
}
?>

2 comments Tags:

Using a test database with Drupal unit tests

January 16, 2009 · 0 comments

In my last few posts I used Test Driven Development (TDD) to write a very simple Drupal module and showed how TDD helped to keep my custom code decoupled from the Drupal framework. This time I want to take a closer look at the biggest headache I ran into while using TDD with Drupal: handling the test data. When I started to write unit tests for my example module I ran into trouble creating test data inside each test, since I found that PHPUnit stopped executing the test each time there was a failing assertion, meaning that test data weren’t cleaned up after a test failure. I was able to avoid this problem by creating and deleting the test data before and after each test was run using the setup/teardown methods from PHPUnit. But this solution brings along different problems with it:

  • In setup() I need to create all of the test data that every test will use since it is called every time, which becomes a performance problem as the number of tests increases.
  • Teardown() still won’t be called if there are any PHP syntax errors in my test or production code, which happens a lot if I’m really using TDD.
  • There’s no way to create different test data for different tests
  • Worst of all, any existing data in my Drupal database might cause the tests to fail, and vice-versa: the test data might interfere with my development work.

We need a better approach for handling test data. Rather than reinventing the wheel, let’s take a look at the Ruby on Rails framework for some inspiration and see if we can emulate the way Rails handles test data using PHP and Drupal. How does Rails handle test data? First of all, each Rails application has multiple, different databases setup: one for development, one for production, and a third for testing at a minimum. Every time you run a unit test in Rails, the test database is manipulated as follows:

  • Rails deletes the existing contents of the test database, if any.
  • Rails loads your test database with an empty copy of your application’s database schema (tables, columns, indices, etc.).
  • Finally Rails runs each of the unit tests targeting this empty test database, by default each test within a separate database transaction (more on this in my next post).

How can we do this with Drupal? If you take a close look at the SimpleTest module, you’ll see that it uses some tricks to create a test copy of the Drupal schema using the “database prefix” feature of Drupal. While this works fine, I decided to see if I could directly follow the Rails pattern of having a completely separate MySQL database to use for testing. Let’s use PHPUnit directly on Drupal from the command line again as I did before. Here’s a very simple PHPUnit test:

<?php
require_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
class TestDataExampleTest extends PHPUnit_Framework_TestCase
{  
  public function create_test_blog_post()
  {
    $node = new stdClass();
    $node->title = "This is a blog post";
    $node->body = "This is the body of the post";
    $node->type = "Story";
    $node->promote = 1;
    node_save($node);
    return $node;
  }
  public function test_there_is_one_post()
  {
    $this->create_test_blog_post();
    $this->assertEquals(1, db_result(db_query("SELECT COUNT(*) FROM {NODE}")));
  }
}
?>

As I explained in December, we have to run the unit test from the Drupal root folder as follows (replace “modules/test_data_module/TestDataExampleTest.php” with the path to the test file):

$ cd /path/to/my-drupal-site
$ phpunit TestDataExampleTest modules/test_data_module/TestDataExampleTest.php

The “test_there_is_one_post” unit test will create a test blog post record in the node table, and then count the number of nodes in the database and assert that there is exactly one. Obviously this will fail if there are any existing node records in my Drupal database, or if I even just run the test more than once:

$ phpunit TestDataExampleTest modules/test_data_module/TestDataExampleTest.php
PHPUnit 3.2.21 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) test_there_is_one_post(TestDataExampleTest)
Failed asserting that <string:6> matches expected value <integer:1>.
/Users/pat/htdocs/drupal4/TestDataExampleTest.php:19
FAILURES!
Tests: 1, Failures: 1.

This failure is actually good: this test is intentionally dependent on the contents of the test database. Later if we can get this test to pass then we know we have properly initialized the contents of the database without resorting to the setup/teardown solution from last time.

Let’s get started by creating a real test database using the MySQL command line:

mysql> CREATE DATABASE drupal_test DEFAULT CHARACTER SET utf8
       COLLATE utf8_unicode_ci;
Query OK, 1 row affected (0.00 sec)

Now, how can we get Drupal to use this database instead of the normal one? Let’s try converting the $db_url value in settings.php into an array, like this:

$db_url["default"] = 'mysql://user:password@localhost/drupal;
$db_url["test"] = 'mysql://user:password@localhost/drupal_test';

Here I’ve renamed the original $db_url variable to $db_url[“default”], and created a new entry for the test database. Now Drupal can run against either the original database, or the test database as we wish.

The next step is to load the test database with an empty copy of the Drupal database schema. In the Rails world, there are Ruby functions that export the development database schema, and then reload it into the test database. In Drupal, the database schema is created automatically by PHP functions when you install the application for the first time. The SimpleTest module also does the same thing before running its tests. Looking at code in install.php from the Drupal installation process, and also in drupal_web_test_case.php from SimpleTest I came up with this solution:

function create_test_drupal_schema()
{
  include_once './includes/install.inc';
  drupal_install_system();
  drupal_install_modules(drupal_verify_profile('default', 'en'));
  $task = 'profile';
  default_profile_tasks($task, '');
  menu_rebuild();
  actions_synchronize();
  _drupal_flush_css_js();

  variable_set('user_mail_status_activated_notify', FALSE);
  $account = user_load(1);
  $merge_data = array('name' => 'admin', 'pass' => 'test', 'roles' => array(),
                     'status' => 1);
  user_save($account, $merge_data);  
}

Here drupal_install_system() and drupal_install_modules() will create most of the empty tables we need just the way they do when you install Drupal. The other calls I took from DrupalWebTestCase->setup() in SimpleTest to create some initial data Drupal requires to function properly, like the menu for example. The last few lines I wrote to setup the admin user properly, and to avoid sending emails to the admin during this process.

The last piece of the puzzle is to find a way to clear out the test database before each test run. To do that, I wrote this code to iterate over all of the tables in the test database and drop them:

function drop_test_tables()
{
  each_table('drop');
}
function each_table($table_callback)
{
  global $db_url;
  $url = parse_url($db_url['test']);
  $database = substr($url['path'], 1);
  $result = db_query("SELECT table_name FROM information_schema.tables
                      WHERE table_schema = '$database'");
  while ($table = db_result($result)) {
    $table_callback($table);
  }
}
function drop($table)
{
  db_query("DROP TABLE $table");
}

To put it all together we just need to call db_set_active(“test”) and call all of this code before our test runs:

db_set_active("test");
drop_test_tables();
create_test_drupal_schema();

Here db_set_active(“test”) tells Drupal we want to use the test database instead of the actual database. After switching to the test database we drop any existing tables that may exist there, and then create a new, empty Drupal schema.

To avoid cluttering my PHPUnit test file, and to be able to reuse this code in many PHPUnit tests, I moved the test database setup into a new include file called: phpunit_setup.inc. I also added some validation code to phpunit_setup.inc to perform a sanity check so you don’t accidentally drop all of the tables in your main Drupal database, and to make it easier to avoid mistakes with $db_url in settings.php. The code requires that "test" be present in the test database name. I also added code to enable database transactions in the test database, which I will discuss in my next post.

To try this out on your system, just download phpunit_setup.inc and then include it at the top of your PHPUnit test file, like this:

<?php
require_once './includes/phpunit_setup.inc';
class TestDataExampleTest extends PHPUnit_Framework_TestCase
{  
  public function create_test_blog_post()
  {
    $node = new stdClass();
    $node->title = "This is a blog post";
    $node->body = "This is the body of the post";
    $node->type = "Story";
    $node->promote = 1;
    node_save($node);
    return $node;
  }
  public function test_there_is_one_post()
  {
    $this->create_test_blog_post();
    $this->assertEquals(1, db_result(db_query("SELECT COUNT(*) FROM {NODE}")));
  }
}
?>

The require_once statement above assumes you downloaded phpunit_setup.inc into the includes folder. If you put it somewhere else, just update require_once as necessary. Now the test passes every time:

$ cd /path/to/my-drupal-site
$ phpunit TestDataExampleTest modules/test_data_module/TestDataExampleTest.php
PHPUnit 3.2.21 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test)

One important detail I’ve glossed over here is that the test only passes because it is the only test I’m running at all in this database. If there were a second test with it’s own test data and assumptions about what data were present, then there would be failures depending on what data each test expected, and which test ran first. Next time, I’ll show how Rails solved this problem using database transactions and show how to use them with Drupal unit tests.

0 comments Tags:

TDD keeps your PHP code separate from Drupal

January 09, 2009 · 0 comments

Many Drupal projects that I’ve been involved with during the past few years have all suffered from different variations on the same problem: the custom PHP code added by me and the development team I was in has ended up being highly coupled with the Drupal framework. At best, our code was hard to understand without knowing a lot about Drupal internals and could never run on its own outside of Drupal. At worst, our code changes were made right inside Drupal core functions and needed to be identified, reimplemented and merged into every newer version of Drupal whenever we decided to upgrade.

In hindsight, I believe that we could have avoided all of these problems if we had only used Test Driven Development (TDD). Using TDD has a lot of benefits for any software project which I won’t go into here (see Wikipedia or Bob Martin's explanation for more information) but I believe the biggest benefit of using TDD specifically on a Drupal development project is that it makes it easy to write PHP code that is not highly coupled with Drupal, is easy to distinguish from Drupal’s core modules, and can even run on its own outside of Drupal entirely.

The same is probably true while developing inside any application development framework.

To explore this topic in detail and find out whether I was right about this, in December I started experimenting with Drupal and TDD. After I wrote my first Drupal PHPUnit test, I started to write a simple example module using TDD, showing how the test first thought process can work with PHP and Drupal. Below I’ll take a look at the finished example module to see whether the module’s code is any less coupled to Drupal, and whether it’s any easier to identify and maintain.

But first let’s take a closer look at the 3 types of PHP code any Drupal web site will contain:

  1. Drupal Code: This is the PHP code you download from drupal.org: files such as node.module, common.include, index.php, etc. You can assume it all works properly, was tested by the Drupal community and is used in thousands of other web sites successfully.
  2. Custom Code: This is code you write to actually implement the special behavior your business or community needs in its web site. You need to write and maintain this, and you need to test that it works properly since no one else will use it.
  3. Connection Code: This is code you have to write to connect your custom code to Drupal. Unfortunately, there’s no way to magically drop your business logic into a Drupal site and see it appear in a web page, ready for users. You need to know something about how Drupal actually works, and you need to connect your Custom Code with the Drupal Code properly.

A good way to understand better the difference between Drupal, Custom and Connection code is to imagine that next year you decide to upgrade your site from Drupal 6.x to Drupal 7, or that you decide to rebuild your site with Ruby on Rails or some other new technology. What you would have to do is:

  • Discard the Drupal Code, and replace it with the new framework.
  • Discard your Connection Code, and write the necessary new code required to connect to the new framework.
  • Take your Custom Code with you, possibly retaining the PHP unchanged for Drupal 7, or else rewriting it using Ruby or some other language.

Clearly you need to know which code is which or else you’ll discard code you really need! TDD can make this easy; the reason why is that using PHPUnit forces you to structure your Custom Code so that it can run outside of Drupal from the command line. Knowing that your Custom Code can run outside of Drupal inside of a unit test suite guarantees that you will be able to take the code with you and later connect it to another framework.

Let’s see how my example module turned out; see one of my previous posts for more background on what this module does and how to set it up. Here’s the finished code:

What I’ve done since my last post while finishing up this simple example is to separate the Custom Code and Connection Code into two separate files. First, here’s my finished Custom Code:

<?php
function tdd_search_for_titles($query, $ascending, $from, $count) {
  $titles = array();
  if ($query != NULL) {
    $sql = "SELECT title FROM {node} WHERE title LIKE '%%%s%%'";
    if ($ascending) {
      $sql .= ' ORDER BY title';
    }
    else {
      $sql .= ' ORDER BY title DESC';
    }
    if ($from || $count) {
      $result = db_query_range($sql, $query, $from, $count);
    }
    else {
      $result = db_query($sql, $query);
    }
    while ($node = db_fetch_object($result)) {
      $titles[] = $node->title;
    }
  }
  return $titles;
}
?>

This probably could be refactored even more into 2 or 3 simpler methods, but the question here is whether this is Custom Code, or Connection Code. I know this is my Custom Code because:

  • This is the code that actually implements the behavior I am trying to achieve. In this simple example the code is searching for nodes by words in their titles.
  • This code is not at all coupled to the Drupal framework. This function is easy to understand for any PHP developer, even someone who knows nothing about Drupal. The only minor exception here is that it uses 3 simple utility Drupal functions: db_query(), db_query_range() and db_fetch_object(). However, these are self-explanatory; If I were a purist, I could have used the mysql_query() function instead, and have eliminated all of my dependencies on Drupal entirely. The code also assumes the presence of the node table and the title column within it, but the same code would work on any database table containing web pages and their titles with only trivial changes. The point is that the code is easy to understand and would be easy to migrate to another framework.
  • This code can run outside the Drupal framework. I know this is the case since I do this when I run PHPUnit on my tests in TddTests.php:
    $ cd ~/htdocs/drupal3
    $ phpunit TddTests modules/tdd/TddTests.php 
    PHPUnit 3.2.21 by Sebastian Bergmann.
    ............
    Time: 0 seconds
    OK (12 tests)

The test code, which I won’t repeat here, is also an essential part of my Custom Code, since it is the only way I have to prove that my function is working properly. It also documents my code’s the desired behavior, and finally will allow me to validate that the code is working if I ever move it to a newer version of Drupal or to some other technology. Also, if you take a look at TddTests.php, you’ll see that the test code is also not highly coupled to Drupal. There are a few references to node_save() and drupal_bootstrap() for example, but most of the test code is pure PHPUnit and has nothing to do with Drupal.

Let’s take a look at my Connection Code, which is in the tdd.module file. I won’t repeat all of it here in this page, but if you look at tdd.module you can see the code does not have these 3 qualities:

  • This code has nothing to do with the behavior I’m trying to implement. It simply provides URL parameters from the user’s request to tdd_search_for_titles(), and displays the results from tdd_search_for_titles() in a web page. It also handles the complexity around the Drupal Form API.
  • This code is very coupled to the Drupal framework. For someone who doesn’t understand Drupal internals tdd.module is very hard to understand – even for developers with years of PHP experience.
  • This code could never run outside of the Drupal framework. This is because it uses functions such as drupal_get_form() and theme() that would be impossible - and pointless - to implement outside of Drupal. More importantly, the functions in tdd.module are called by Drupal at certain times during its processing; none of this would make any sense outside of Drupal.

Here’s the only interesting snippet from my Connection Code:

...
$sortAscending = true;
if (isset($_GET['sort']) && $_GET['sort'] == 'desc') {
  $sortAscending = false;
}
$titles = tdd_search_for_titles($keys, $sortAscending, 0, 10);
$rows = array();
foreach ($titles as $title) {
  $rows[] = array($title);
}
...

This is the actual location in the code where we “connect” from Drupal to my Custom Code, and vice-versa. This few lines are actually passing (connecting) the request parameters onto my Custom Code, and later saving the results in $titles which is parsed and returned to Drupal in the required format.

In my next post I’ll try using TDD again to write a more complex and interesting Drupal module: one that will display monthly archive links - e.g. January 2009 (23) - for a Drupal blog, similar to what you would see in a standard WordPress or B2Evolution blog site.

0 comments Tags:

Using TDD to write a Drupal module

December 19, 2008 · 1 comment

Last time I wrote my first PHPUnit test for Drupal. Now I’d like to continue writing my new Drupal module using Test Driven Development (TDD). Here are links to the finished test code and production code files that I’ll write below: TddTests.php and tdd.module, in case you want to download and try these tests yourself.

With TDD the first step is always to write a failing unit test. But, which test to write first? What should I try to test? Since the demonstration module I’m writing will display a series of nodes on the screen containing a certain word in their title, let’s try to test that behavior directly, right away:

public function test_search_for_titles()
{
  $query = 'FindMe';
  $titles = tdd_search_for_titles($query);
}

This doesn’t actually test anything, but it calls a function that will return the titles. Now if we run this test, it will obviously fail since “tdd_search_for_titles” is not defined. Let’s write that. Another rule of TDD is to write just enough production code to get the failing unit test to pass. The simplest way to get this test to pass is to just return a hard coded title like this:

function tdd_search_for_titles($query) {
  return array('Hard coded title with the word FindMe');
}

Now the test above passes, along with the first test I wrote in my last post:

$ phpunit TddTests modules/tdd/TddTests.php 
PHPUnit 3.2.21 by Sebastian Bergmann.
..
Time: 0 seconds
OK (2 tests)

This seems silly, but at least we’ve started writing and executing code in our new module. Since our test is not actually testing anything, let’s add some real test code to it by checking that the titles returned actually contain the query string:

public function test_search_for_titles()
{
  $query = 'FindMe';
  $titles = tdd_search_for_titles($query);
  foreach ($titles as $title) {
    $this->assertTrue(stripos($title, $query) > 0);
  }
}

Run the test again: it still passes. It’s important when using TDD to continuously run your tests as you write code, as often as every 30 seconds or 1-2 minutes. That way as soon as your tests fail, you know immediately what caused the problem: whatever code you changed during the past 1-2 minutes. One benefit of TDD is that you rarely need to use a debugger to figure out what is wrong since you execute and test your changes so frequently. Next, as a sanity check, let’s try breaking the code on purpose and checking if our test is really working or not:

function tdd_search_for_titles($query) {
  return array('Hard coded title with the word FindXYZMe');
}

Now the test fails (good!):

$ phpunit TddTests modules/tdd/TddTests.php
PHPUnit 3.2.21 by Sebastian Bergmann.
.F
Time: 0 seconds
There was 1 failure:
1) test_search_for_titles(TddTests)
Failed asserting that <boolean:false> is true.
/Users/pat/htdocs/drupal3/modules/tdd/TddTests.php:15
FAILURES!
Tests: 2, Failures: 1.

I frequently do something like this when some new tests pass for the first time, just to be sure they are really executing and calling the code I intended them to. Sometimes trusting your tests too much can be dangerous! If we remove the “XYZ” change the test will pass again. Once the test is passing again we can move on.

So far we haven’t done much. All we have done is to prove that we can write a function to return a hard-coded string. Let’s continue by testing that we can query actual data in the MySQL database. But first we need to create some data for our function to look for. The simplest way to do that would be to open Drupal in a web browser and to create some nodes (web pages) that have the query string in their title. This is a good way to get started, but can lead to trouble down the road since our test will now rely on someone having created these pages manually. If we run the tests using a different database or on someone else’s machine they will fail. A better approach would be to have the test itself create the data it expects. Here’s how to do that:

public function test_search_for_titles()
{
  $login_form = array('name' => 'admin', 'pass' => 'adminpassword');
  user_authenticate($login_form);
  
  $node = new stdClass();
  $node->title = 'This title contains the word FindSomethingElse';
  $node->body = 'This is the body of the node';
  node_save($node);

  $query = 'FindSomethingElse';
  $titles = tdd_search_for_titles($query);
  foreach ($titles as $title) {
    $this->assertTrue(stripos($title, $query) > 0);
  }
  
  node_delete($node->nid);    
}

There are a few different things going on here:

  • Most importantly we have created a node object in memory, loaded it with some test values, and then saved it into the database using node_save(). This means that every time the test is run, it will create the node that the test code expects to find.
  • The call to user_authenticate() at the top is required to setup Drupal properly, allowing the node_save() and node_delete() functions to work. Without it, we would probably get an access denied error since anonymous users typically aren’t allow to create and save pages, and certainly not to delete pages.
  • Finally, at the bottom of the test we are calling node_delete(), and passing the node id of the node we created earlier. This is how the test cleans up after itself. Without this our Drupal database would begin to fill up with test records, one new record each time we run the test.

If we run the test it will fail, of course, since our hard coded title does not contain “FindSomethingElse.” Instead of changing the hard coded string to make the test pass, let’s actually do some real coding and write a SQL statement to get the titles from MySQL:

function tdd_search_for_titles($query) {
  $titles = array();
  $result = db_query("SELECT title FROM {node}");
  while ($node = db_fetch_object($result)) {
    $titles[] = $node->title;
  }
  return $titles;
}

Finally we have started writing code that our module will actually use. Here we take the query string, construct a select statement and execute it using db_query(). Once we have the results, we return the titles as an array of strings. Let’s run our test and see what happens:

$ phpunit TddTests modules/tdd/TddTests.php
Failed asserting that <boolean:false> is true.

Oops — it failed! What went wrong? If we add another check to the test, we can get some more information:

$this->assertEquals(count($titles), 1);

And if we run again:

$ phpunit TddTests modules/tdd/TddTests.php
Failed asserting that <integer:1> matches expected value <integer:27>.

The problem is that there are 27 titles returned, instead of just the one we created. If we take a second look at the SQL statement we see right away that there is no WHERE clause, causing all of the titles in the database to be returned. Now, if we fix the SQL statement, the test should pass:

$result = db_query(
  "SELECT title FROM {node} WHERE title LIKE '%%%s%%'", $query);

Let’s see:

phpunit TddTests modules/tdd/TddTests.php
Failed asserting that <integer:1> matches expected value <integer:3>

Now what’s the problem?? It turns out that both the test and production functions are working properly, and there really are 3 nodes in the database with “FindSomethingElse” in the title. The reason why is that our call to node_delete() was not executed when we ran our test above and it failed (twice). We’ll fix that below. For now, let’s just clean up the database and remove the extra test records. The best thing to do is just to delete them from the Drupal admin console (Administer->Content management->Content). Now when you are sure no other nodes exist with “FindSomethingElse” in the title run the test again and it will pass.

Now… why were extra test node records created when the test failed? What happened is that the call to node_delete() is never executed if any of the asserts fail. That is, the test execution stops as soon as there is a failing assert statement. I suppose this makes sense; we know the overall test will fail if there is even one failing assert statement, and so there’s no need to continue executing the test code.

The solution is to refactor the test code and use two new functions called setup() and teardown(), as follows:

public function setup()
  {
    $login_form = array('name' => 'admin', 'pass' => 'adminpassword');
    user_authenticate($login_form);

    $this->node = new stdClass();
    $this->node->title = 'This title contains the word FindSomethingElse';
    $this->node->body = 'This is the body of the node';
    node_save($this->node);    
  }
public function teardown()
  {
    node_delete($this->node->nid);  
  }

The way this works is that setup() is called once for each test in the test class, just before the test function is called. In our case it will be called twice: once for test_tdd_help() and once for test_search_for_titles(). This gives us a chance to create test data and perform other setup tasks before each test is executed. As you might guess, teardown() is called once for each test also, right after the test function finishes. This gives us a chances to remove our test data, even if the test fails. One thing to note about this: you have to save the $node object inside the test class so that it can be accessed from teardown() and from the tests themselves if necessary; so “$node” becomes “$this->node”.

Here are the links again to the final test and code files: TddTests.php and tdd.module. In my next post I’ll finish up the code for tdd.module and TddTests.php without providing as much detail, and then move quickly to a discussion of how this code can be integrated with Drupal and included in a working web site.

1 comment Tags:

Writing your first PHPUnit test in Drupal

December 12, 2008 · 4 comments

In my previous post I wrote some typical Drupal code that lists nodes containing a given word in their title. In some upcoming posts I’ll rewrite that module using Test Driven Development and see whether the module turns out differently. But to get started with TDD in Drupal we need to be able to write our first test. Before we can even do that we need to install PHPUnit. Once you have PHPUnit installed, test that it is working properly by writing a trivial test file called FirstTest.php somewhere on your hard drive:

<?php
class FirstTest extends PHPUnit_Framework_TestCase
{
  public function test_two_plus_two_is_four()
  {
    $this->assertEquals(2+2, 4);
  }
}
?>

To run this you should use PHPUnit as follows from the same folder containing the test file:

$ phpunit FirstTest
PHPUnit 3.2.21 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test)

If you get errors running this command then review the install instructions for PHPUnit and install it again if necessary.

Next: how do we apply PHPUnit to Drupal? Let’s create a new module folder, and an “info” file in it so Drupal knows what our new module is called: “tdd” for example. So we create a folder drupal/modules/tdd and a tdd.info file in this folder that contains:

name = tdd
description = Module written with TDD
package = TDD Demo Modules
version = VERSION
core = 6.x

Now let’s write our first test. PHPUnit convention is that if you have a PHP class called XYZ, you would write a test class for it called XYZTests, and by default place the test class in XYZTests.php in the same folder. Since we’re using Drupal and don’t have any object oriented code, let’s just call our test class TddTests, named after our new module, and put it into a new file in the same folder called TddTests.php. It needs to be a subclass of PHPUnit_Framework_TestCase like this:

<?php
class TddTests extends PHPUnit_Framework_TestCase
{
  public function test_tdd_help()
  {
    $this->assertEquals(
      tdd_help('admin/content/tdd'), "<p>Help for TDD module.</p>");
  }
}
?>

This test will call our new module’s “help” function with the path to the module’s new page and make sure we get the proper help. This seems like a good first test to write, since it’s very simple but still proves that we can call Drupal code from PHPUnit. Let’s run it and see what happens… But first you have to cd to the folder containing the TddTests.php file. PHPUnit also assumes that by default you’re executing the tests from the folder containing the test file. Let's try running our test:

$ cd modules/tdd
$ phpunit TddTests.php 
PHPUnit 3.2.21 by Sebastian Bergmann.
Fatal error: Call to undefined function tdd_help() in
/Users/pat/htdocs/drupal/modules/tdd/TddTests.php on line 6

Obviously the test fails because we haven’t written our new module yet. This seems like a waste of time, but we've taken the first step in the TDD cycle: write a failing unit test. Now let's start writing tdd.module:

<?php
function tdd_help($path, $arg) {
  switch ($path) {
    case 'admin/content/tdd':  
      return '<p>Help for TDD module.</p>';
  }
}
?>

If you run this test again you’ll get the same error message. So now what’s wrong? We forgot to enable our new module in the Drupal admin console, so the “tdd_help” function is still undefined. To enable it, open the Drupal admin console, go to the Administer->Site Building->Modules page and look at the bottom for a section called “TDD Demo Modules.”

But now if you run PHPUnit you’ll still get the same error. The mostly empty “TDD” module is now setup and working inside of Drupal, but PHPUnit has no idea what Drupal is or how to load it. What we need to do is declare somehow in TddTests.php to include and initialize the Drupal framework. To get this to work, we can use the index.php file in the root folder of Drupal app as an example. This is the PHP file that handles all incoming requests to Drupal by initializing the framework, executing the proper menu callback and displaying the result. If you look at the top of the index.php file, you’ll see these two lines:

require_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

If we copy these 2 lines into TddTests.php like this:

<?php
require_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
class TddTests extends PHPUnit_Framework_TestCase
{
  public function test_tdd_help()
  {
    $this->assertEquals(
      tdd_help('admin/content/tdd'), "<p>Help for TDD module.</p>");
  }
}
?>

… and try to run our test again we get:

$ phpunit TddTests
Warning: require_once(./includes/bootstrap.inc): failed to open stream:
No such file or directory in /Users/pat/htdocs/drupal/modules/tdd/TddTests.php on line 2
Fatal error: require_once(): Failed opening required
'./includes/bootstrap.inc'
(include_path='.:/usr/local/php5x/lib/php') in
/Users/pat/htdocs/drupal/modules/tdd/TddTests.php on line 2

The problem now is that the include line has the wrong relative path. If you corrected it you would still get more errors trying to include other Drupal files. It turns out that the drupal_bootstrap function assumes that the current directory is set to the root folder of your app: the location of index.php. To get it all to work, we just need to execute the tests from the root folder:

$ cd ~/htdocs/drupal
$ phpunit modules/tdd/TddTests.php 
PHPUnit 3.2.21 by Sebastian Bergmann.
Class modules/tdd/TddTests could not be found in modules/tdd/TddTests.php.

This last error appears because we aren’t using the PHPUnit command line properly. The first parameter is the test class name; the second optional parameter that we need to use now is the test file path. Here’s the proper command line to use:

$ phpunit TddTests modules/tdd/TddTests.php 
PHPUnit 3.2.21 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test)

Finally we’ve successfully written and executed our first test! More than that, this is our first step toward writing a Drupal module using TDD. Next time I’ll get to work writing the rest of the module using TDD with these files as a starting point.

4 comments Tags:

Example Drupal module to use for TDD demonstration

December 08, 2008 · 0 comments

To illustrate how to use Test Driven Development while developing a Drupal module, we first need an example module to write: Let's assume we want a page in our Drupal admin console that lists all of nodes in the database, similar to the Administer->Content Management->Content page. It will also filter and sort the list, but instead of filtering on the status and type of nodes, it will filter on words contained in the node title.

The first thing I did was to write the module as quickly as possible as I normally would have without using TDD. Here’s the completed module: demo.module, and demo.info. If you want to follow along this demonstration just install a fresh copy of the latest version of Drupal 6.x (Drupal 6.6 when I wrote this); be sure to create and use a new MySQL database to use in these examples so your existing work won’t get in the way. Then download the demo.module and demo.info files into a new "demo" module folder and enable the demo module in your admin console (Administer->Site building->Modules). It will be called "Demo" and will appear at the bottom of the list of modules in a section called "TDD Demo Modules."

Once the Demo module is enabled, look for a new page in your admin console at Administer->Content management->Demo Page. It will initially display a list of all the nodes in your database, and also allow you to enter a keyword to show only the nodes whose title contains the given word.

If you are using a new empty database, create a couple of pages (Create Content->Page) to have some data to display here. I created two pages with these titles (body text doesn't matter):

  • This is my first page
  • My second page!

A quick review of the module code will reveal nothing special. Demo.module is very simple… it has some boilerplate code (demo_help and demo_menu), and then displays the new “Demo Page” with the demo_page_view function:

function demo_page_view($keys = NULL)
{
  $header = array(
    array('data' => t('Page Title'), 'field' => 'title', 'sort' => 'asc')
  );
  $sql = "SELECT * FROM {node} WHERE title LIKE '%%%s%%'";
  $sql .= tablesort_sql($header);
  $result = pager_query($sql, 50, 0 , NULL, $keys);
  $rows = array();
  while ($data = db_fetch_object($result)) {
    $rows[] = array(check_plain($data->title));
  }
  if (empty($rows)) {
    $rows[] = array(array('data' => 'No pages match the given pattern.'));
  }
  $output = drupal_get_form('demo_pattern_form', $keys);
  $output .= "Pages matching this pattern:";
  $output .= theme('table', $header, $rows);
  $output .= theme('pager', NULL, 50, 0);
  return $output;
}

This is really all there is to demo.module – this function generates a SQL statement using tablesort_sql, then selects all the matching nodes from the database using pager_query and builds an array containing the matches. Then it returns the HTML for a table containing the matching records using the currently selected theme. Later in the module file there are more functions that handle navigation and processing for the “demo_pattern_form.”

This code is so simple it’s hard to imagine that anything could be wrong with it: in just a few lines it gets the data we need and also displays it. It’s also very typical Drupal module code. If you look around the Drupal code base or at code from 3rd party modules you will see a lot of code that looks just like this. It uses common Drupal functions like “theme,” “pager_query” and “db_fetch_object.”

But in my opinion there are a two things wrong with this example module:

  • There are no tests – if I needed to change something about the behavior of demo.module I would have no way to know whether or not I broke something after making a code change, other than by opening a browser and testing manually.
  • It’s very hard to separate the actual business logic or custom behavior that demo.module provides from the boilerplate code required to run inside of Drupal.

A veteran Drupal developer would disagree with the second point, of course: obviously the SQL statement used in demo_page_view really is the only custom behavior here, and all of the other calls to Drupal functions like drupal_get_form, table_sort_sql, etc., are used to help display our data in a Drupal page. But imagine a PHP developer who wasn’t yet very familiar with Drupal looking at this module file for the first time: how would she or he have any idea what this module did? Or where to begin to change it’s behavior if necessary? This is another important use of tests: as living documentation for what code does and how it should work.

This is a silly example, but imagine if this module did something sophisticated and important to your business. Then imagine if your business decided to upgrade from one version of Drupal to another – or imagine that your business decided to use DJango, Joomla or some other CMS system… or to implement it using custom Java or Ruby code. Then the question would become: what part of the module’s code is ours that we want to keep? And what part of the module is simply needed to coexist with Drupal? Right now it’s not so easy to tell.

I believe that using TDD while writing a Drupal module will not only provide the normal benefits of testing: emergent design, living documentation and simply more robust code, but will also lead to isolating your business’s code from the framework in a natural way. By writing unit tests first, you will have to think about what you are trying to test: your business logic only and not the Drupal framework itself.

In my next post we’ll get started by installing PHPUnit and writing our first PHPUnit unit test with Drupal.

0 comments Tags:

Why to use TDD with Drupal

September 02, 2008 · 4 comments

Drupal is a very popular Content Management System (CMS) developed using PHP. It’s a great way to get an interactive, community web site such as a blog or discussion forum up and running quickly without writing any code. But Drupal is a poor choice for general, complex web application development for a few simple reasons:

  1. It doesn’t use a standard, general software architecture such as Model-View-Controller (MVC) like Rails or Struts.
  2. Drupal doesn’t employ or support modern development best practices such as Test Driven Development (TDD) or automated testing more generally. (Exception: the SimpleTest module can provide some automated integration testing using HTTP without opening a real browser.)
  3. While the module API does allow for some customization, it can be easy to get into trouble if your target application is outside of Drupal’s design center. Specifically, you may end up having to modify Drupal’s core code or database schema when Drupal doesn’t behave the way you would like.

A better choice for more general or complex web development would be Ruby on Rails.

If you do need to develop a custom web application using the Drupal framework, you can avoid problems #2 and #3 by using a test driven approach. Instead of simply implementing your custom logic by writing PHP code to the Drupal module API, try pulling your custom code out of Drupal entirely and running it independently of the framework. This will insure that your custom code could someday be run with another web framework and also help you avoid the temptation of modifying Drupal’s core code directly. Of course, you will also get all of the normal benefits of TDD: better design oriented around business requirements, live documentation through executable code, etc.

How to do this? Write tests first using PHPUnit (which fail), then get your tests to pass by implementing the desired behavior using PHP 5 objected oriented code. Lastly, write a “shell” module that calls out to your test driven object oriented PHP code from Drupal. This means your code will be run twice: once from the command line by PHPUnit during development and from your CI process, and again from Drupal to process end user requests in production.

In a series of future posts I'll write a Drupal module using TDD, using a few tips and tricks along the way to get PHPUnit and Drupal to play together nicely.

4 comments Tags: