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.
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.
Here is the output generated by running all of the tests in the Node tests group.
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.
/**
* 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:
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.
// 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:
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:
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.
// 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!
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.
$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.
$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.
$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.