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.
Tags:drupal
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.
Tags:drupal
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.
Tags:drupal