by Robert Douglass on November 26, 2007 // Short URL

An Introduction to Unit Testing in Drupal

This article applies to Drupal 5.3, the Simpletest module version 5.x-1.0, and the Simpletest project from Sourceforge.net, version simpletest_1.0.1beta2.

What is unit testing

Unit testing is the art and practice of taking a small portion of code, a unit, and subjecting it to programmatic tests to prove its correctness. The smallest units in PHP code are usually functions, and the unit tests will run the functions with sets of controlled parameters, and then make assertions about the return values of the function, or about the state of the application before and after the function has run. For example, if the function is designed to validate email addresses, the unit tests would pass in known valid email addresses and assert that they validate correctly. The tests would also pass in a number of invalid email addresses as well and assert that they do not validate.

Why is unit testing useful?

Writing unit tests helps produce higher quality code on many levels.

  • The availability of tests helps detect the introduction of bugs whenever the programmer adds new features or refactors the code. This is called regression testing.
  • Tests also serve as a source of documentation about what code is really expected to do.
  • The act of writing the tests challenges the programmer to consider possible edge cases and their consequences.
  • Last but not least, the act of writing tests encourages the programmer to write code in small chunks that can be tested independently.

The last point is worth diving into. If we are writing code that needs to do three manipulations to an incoming string, the temptation is to write a function where the string comes in as an argument, the three manipulations are executed, and the resulting string is returned. When this function isn't working right it is hard to determine which of the three manipulations are failing. Your test case can only send a string in and compare the result with the expected result. How can you know which of the manipulations isn't working correctly? Breaking the function into smaller pieces, one for each manipulation, results in code that is easier to test. Test cases can then be written for each manipulation, better isolating the source of failure.

Unit tests can be often be scripted to run automatically, making them an ideal part of your development, build, and deployment process. You can run tests before committing new code to the repository. You can run the tests after adding new modules and thus ensure that previous functionality hasn't been compromised. You can run tests after deploying changes from a development environment to a staging environment thus cutting down on the dependency of trial and error to detect problems.

What testing tools does Drupal offer?

The principal tool for unit testing in drupal is the Simpletest module. This module is a Drupal extension to the Simpletest project for general PHP unit testing. The Drupal module adds a wealth of tools and convenience for Drupal specific unit testing. For example, it has the ability to create new users and nodes, set configuration variables, and submit Drupal forms.

As an addition to the Simpletest module there is the Simpletest automation project which provides a system for running test suites automatically and reporting the results. The Simpletest automation is also capable of applying patches to Drupal code, and is thus an essential tool for vetting patches in the issue queue.

The similarly named but very different Simpletest automator extends the convenience of writing tests even further by allowing you to click through your site as it records your actions as a macro. This macro can then be used as the basis for a unit test that you can run automatically at a later time.

How to set up the Simpletest module

To begin running unit tests on your Drupal installation, download the latest release of the Simpletest module and install it as you would any other module. Please note that you should not install this module on a production site. It is only designed for development and staging purposes, and running the tests will alter the state of your database. Running the simpletest unit tests on a production site could lead to lost data or unpredictable behavior.

The Drupal module depends on the Simpletest library, which you can download from Sourceforge.net. The latest release, as of this writing, is simpletest_1.0.1beta2. The download from Sourceforge comes as a tarball which you need to extract into the simpletest module directory. The resulting directory structure looks like this.

simpletest-directory-structure-graffle.png

That concludes the installation of the Simpletest module, and you are now ready to run the existing unit tests.

How to run the included tests

The existing unit tests can be found under Administer->Site building->Simpletest unit testing (admin/build/simpletest). This page is a listing of all the test suites that are installed. The bulk of them come from the Simpletest module itself, and are used to test Drupal core functionality. Since it is possible for any module to provide test suites it is entirely possible that some of the contributed modules you have installed will also have test suites. Some modules that include test cases are the coder, organic groups and timeline modules.

The Simpletest unit testing page allows you to select all of the tests in any group, or, if you expand the Tests fieldset in any group, the single tests individually. Select the Run selected tests option at the bottom and click Begin, and Simpletest will do its work. Depending on how many tests you've chosen, the running time may be anywhere from a couple of seconds to minutes. Here I am about to run all of the tests in the "Node tests" group.

node-tests-selected.png

Here is the output generated by running all of the tests in the Node tests group.

test-results-graffle.png

What to do if you get errors

The beauty of having unit tests available is that it makes error reporting much easier. If you run the test suites for Drupal core or a contributed module and get Fails, you have a great opportunity to use the Drupal.org issue queue to file a bug report. Paste the output from Simpletest into the issue and the module maintainer will know exactly what it is that went wrong. Be sure to include the ordinary information about your Drupal installation, including what release you are running and what modules you have installed.

How to write a basic unit test

Unit testing is a great productivity enhancer for programmers and I highly recommend using it as a core technique whenever you write code. Your code will come together quicker and will be higher quality for the effort. Adding unit test support to your Drupal module is easy. Simpletest defines hook_simpletest which your module implements.

<?php
/**
* Implementation of hook_simpletest().
*/
function hook_simpletest() {
 
$module_name = 'mymodule'; // Change this to your module name.
 
$dir = drupal_get_path('module', $module_name). '/tests';
 
$tests = file_scan_directory($dir, '\.test$');
  return
array_keys($tests);
}
?>

The hook returns a list of files that contain test cases. The convention is to make a tests directory in your module and put the test cases in there. If you follow the convention then you only need to copy the code above and change the function name and update the $module_name variable to be the name of your module.

Now you can start to create test cases. These files should have the ending .test and reside in the tests directory in your module. The stub code for a test case looks like this:

<?php
class PageViewTest extends DrupalTestCase {

  function
get_info() {
    return array(
     
'name' => t('Page node creation'),
     
'desc' => t('Create a page node and verify its consistency in the database.'),
     
'group' => t('Node Tests'),
    );
  }

  function
testSomething() {
  }

  function
testSomethingElse() {
  }

}
?>

A test case is a class that extends the DrupalTestCase class. You must implement the get_info() method which returns a keyed array with 'name', 'desc' and 'group' strings which are used for display on the Simpletest unit test page. Beyond that, any function that starts with 'test' will be executed whenever the test case is run.

The Simpletest framework and the DrupalTestCase offer a lot of handy tools for executing your tests. We'll explore a couple of these by looking at some non-trivial examples taken from the test cases found in existing modules. The first example comes from the user_validation.test suite from the Simpletest module.

<?php
 
// username validation
 
function testMinLengthName() {
   
$name = '';
   
$result = user_validate_name($name);
   
$this->assertNotNull($result, 'Excessively short username');
  }
?>

This test creates a $name which is unacceptable as a user name because it is an empty string. It passes the $name into Drupal's user_validate_name function and then uses the assertNotNull() method to check whether the test passes or fails. The assertNotNull() method must be called using the $this object, which is an implicit self reference in any PHP class object. The assertNotNull() method will check whether $result is NULL. If it is NULL, the test fails. If it is not NULL, it passes. This makes sense because the function is asserting that the object is not NULL, which is another way of saying "I expect this to have a value (the error message saying that the validation fails), please fail if not NULL". The assertNotNull() method also takes a message parameter. This message gets passed on to the Simpletest framework and is displayed on the test results page. Here is the outcome of the test listed above:

user-validation-pass.png

As you can see, the test passed, which means user_validate_name() did the expected and returned a non NULL value when the $name variable was unacceptable. If you refer to the API documentation for user_validate_name, you will see that it returns string values whenever validation fails.

The next example comes from the tests for the finduser module. The goal of the module is to search for users. In order to test this, the site needs to have users, and the test suite has to know about them, otherwise it wouldn't know what to search for or what to expect. Fortunately the DrupalTestCase has many Drupal-specific convenience methods that let us handle this situation. Here's the code:

<?php
 
function testSearchByEmail() {

   
// Temporarily set the 'finduser_email' variable to TRUE. It will return
    // to whatever it's normal state is when the tearDown() method is called.
   
$this->drupalVariableSet('finduser_email', TRUE);

   
/* Prepare a user that we can search for */
   
$web_user = $this->drupalCreateUserRolePerm();

   
// Search by email
   
$results = finduser_do_search('email', $web_user->mail);
   
// Assert that only one result is found
   
$this->assertEqual(count($results), 1);
   
// Assert that it is the user we created.
   
$this->assertEqual($web_user->uid, $results[0]->id);

   
// Search for a bogus, non-existent user
   
$bogus_results = finduser_do_search('email', 'xxx'. $web_user->mail);
   
// Assert that zero results are found
   
$this->assertEqual(count($bogus_results), 0);
  }
?>

The first convenience method we see here is drupalVariableSet(). Once again, this is a method of the test case object itself, and thus must be called from the $this object. What the method does is inspect the current value for a Drupal variable (finduser_email in this case), take note of it, and then set it to a new value (TRUE). After the tests have run, the variable will be set to its original value, all in the background without you needing to worry about it. In this way you can temporarily change the configuration parameters of your site and not have to worry about cleaning up - the Simpletest module handles it for you.

The next convenience method is drupalCreateUserRolePerm(). This method takes an array of permissions and uses them to create a new user account. The user account will have a generated name and email address, and will have a special user role with the permissions in the array. You can then use this user account to test the functionality of your site. Just like with drupalVariableSet(), the user and the roles will be deleted when the tests are finished running so you needn't worry about filling up your database with extra test users, nor do you need to go and create these users manually.

Once the setup steps are all done the test goes on to invoke the actual function that is being tested: the finduser_do_search() function. The function is told to search for an email that is equal to the $web_user's email. To determine whether the function behaves as expected, $this->assertEqual() is called. assertEqual() takes the first two parameters and asserts that they are equal. The results of assertEqual() will be saved for reporting on the Simpletest unit tests page. The test continues, however, and $this->assertEqual() is called again, this time asserting that the $web_user's name is equal to the username that was found by doing the search.

It is important to test both success cases and failure cases equally. The first two assertions tested that a user was found when expected. The third assertion in the code above asserts that no user is found when searching for a bogus, non-existent user.

<?php
   
// Search for a bogus, non-existent user
   
$bogus_results = finduser_do_search('email', 'xxx'. $web_user->mail);
   
// Assert that zero results are found
   
$this->assertEqual(count($bogus_results), 0);
?>

There are many more ways to do assertions, as well as many more Drupal helper functions. The Simpletest handbook pages on Drupal.org are a good reference for the various tools available.

Functionality testing - a special case

The examples we have looked at so far are true unit tests because they inspect one function at a time, throwing various arguments at it and asserting that the results are correct. The Simpletest framework supports an entirely different method of testing that simulates actions done in a web browser and inspects the output that is sent to the browser, making assertions based on the output. Here is an example that creates a new user, logs into the site using the new user, submits the form to create a new Page node, asserts that the output that gets sent to the browser has the "Your %post has been created" message, and asserts that the node exists in the database. Keep in mind how many clicks it would take you to do all of those steps manually!

<?php
 
function testPageCreation() {
   
/* Prepare settings */
   
$this->drupalVariableSet('node_options_page', array('status', 'promote'));

   
/* Prepare a user to do the stuff */
   
$web_user = $this->drupalCreateUserRolePerm(array('edit own page content', 'create page content'));
   
$this->drupalLoginUser($web_user);

   
$edit = array();
   
$edit['title']    = '!SimpleTest test node! ' . $this->randomName(10);
   
$edit['body']     = '!SimpleTest test body! ' . $this->randomName(32) . ' ' . $this->randomName(32);
   
$this->drupalPostRequest('node/add/page', $edit, 'Submit');

   
$this->assertWantedRaw(t('Your %post has been created.', array ('%post' => 'Page')), 'Page created');

   
$node = node_load(array('title' => $edit['title']));
   
$this->assertNotNull($node, 'Node found in database. %s');
  }
?>

The first new method in this code is $this->drupalLoginUser(). Use this function to log in using any $user object, such as those returned by user_load(). This code logs in using the $web_user which was created with the 'edit own page content' and 'create page content' permissions.

This code submits a form using Http POST. Note that $edit contains only the information that the user would be required to enter on the web form. The title and body fields were generated using the $this->randomName() method. 'Submit' is the name of the button that gets clicked in order to submit the form. The $this object stores the HTML output so that you can make assertions against it later.

<?php
   $edit
= array();
   
$edit['title']    = '!SimpleTest test node! ' . $this->randomName(10);
   
$edit['body']     = '!SimpleTest test body! ' . $this->randomName(32) . ' ' . $this->randomName(32);
   
$this->drupalPostRequest('node/add/page', $edit, 'Submit');
?>

This code takes the HTML output that is returned after submitting the form and looks for a specific string within it. The method asserts that the string exists.

<?php
  $this
->assertWantedRaw(t('Your %post has been created.', array ('%post' => 'Page')), 'Page created');
?>

Finally, this test asserts that the node is found in the database as well.

<?php
   $node
= node_load(array('title' => $edit['title']));
   
$this->assertNotNull($node, 'Node found in database. %s');
?>

More information

For more information on the Simpletest module, please refer to the handbook pages on Drupal.org. The source code for the drupal_unit_tests.php and drupal_test_case.php is also informative as the reference for the available assert and helper methods. The Simpletest API documentation is useful for learning about the underlying framework. Any of the methods available in the Simpletest base classes are also available to your test cases through class inheritance. The center of testing activity for Drupal.org is testing.drupal.org. There are two groups on groups.drupal.org that deal with testing, the Unit testing group and the Quality assurance group.

Robert Douglass

Comments

gman

Excellent Post

I have needing to help push my developer group to embrace unit testing. I have been meaning to look at the Simpletest module for a while and now with this article I can bring it up again in our next developers meeting.

Reply

Andre Molnar

function testMinLengthName()

Nice write up.

You may want to touch up the testMinLengthName() example to match the description you give - or - the other way around. You say that testMinLengthName() asserts Null, but the example code is actually asserting NOT Null.

Just to add to the article, this is actually a good example of things testers should keep in mind: When writing tests make sure that the test is testing what you think it is testing. Get used to thinking in double negatives and doing other logical jumping jacks. A PASS is sometimes a FALSE and a FAIL is sometimes a TRUE.

If you can answer the question "Didn't you not never go nowhere when you were a kid?" AND explain why your answer is TRUE or FALSE - you too can write a unit test!

andre

Reply

Rolf

Javascript/jQuery unit test

Hi,

Great article! From my point of view unit test is fundamental in modern SW development, and Simple test it perfect for that purpose. Please bear in mind that programmers have always made unit tests! However, most programmers keep these unit tests in their head instead of a framework such as Simple test.

During the last couple of months I have done some programming in Javascript for Drupal 5.x, and since jQuery is included in Drupal 5.x it has been fun! However, I havn't been able to find a unit test frame work for Javascript/jQuery in Drupal. Can Simple test be extended to activate javascript unit tests? Or can you recommend at third party tool for that purpose?

Cheers,
Rolf

Reply

robert

Drupal core coverage is less than complete

We need people who are evil minded testers to come forward and write more tests for Drupal core. The proper place for these tests is as a part of the Simpletest module itself.

There is an automated test server that is being tested on the Drupal.org infrastructure. It takes patches from the issue queue, runs tests on them, and reports back to the issue queue with the results.

These initiatives need a lot of support and energy before they see the light of day, so if you're interested, jump on board and lend a hand.

Reply