Using TDD to write a Drupal module

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.