Writing automated tests in Drupal 7

I have written quite a few tests now, both for Drupal core and for the contrib modules which I maintain. See http://qa.drupal.org/pifr/test/27300 for example.

So I thought it's time to write down a part of what I learned to help others. Because writing tests can be tedious (and trying to figure out why a test broke sometimes too!). However, once you have them, they provide a very valuable indicator to tell if you broke something when you refactor something or implement a new feature. In fact, once your project reaches a certain size, having a fair amount of tests is absolutely necessary to do any serious refactoring without introducing a ton of bugs.

Testcase, assertions, ... what?!

A short explanation of common terms used in combination with automated testing:

Term Description
Simpletest Originally a unit testing framework. Has been ported to a Drupal module and is now part of Drupal 7 (The module is just called "Testing" there). Make sure to enable it when you are writing tests.
Test case This is a class that extends from one of the base test classes. It contains the test methods, setUp/tearDown and a static getInfo() method.
Test method A public method that starts with "test". Each test method of a testcase is automatically executed in a separate environment.
Assertion A test assertion is a single check to verify the output of your code. For example, you can compare two variables or look for a certain text on the currently loaded page. Simpletest then automatically collects all these assertions and will show you a table with information about each assertion.
Unit test Unit tests test a single component (part) of your module or project without touching anything else. This usually means direct testing of API functions. Although this might sound easier than integration tests, it is actually often a very hard task in Drupal because of the complex dependencies and that it is hard to use technices like Mocking as Drupal is mostly a procedural framework.
Integration test Integration tests test how different components work together. In Drupal, this typically involves acting like a normal user and GET pages, POST forms, click on links and so on. Simpletest provides a bunch of helper functions that usually make this easier than it sounds. That is also the reason why most automated tests in Drupal core are actually integration tests. A typical example would be creating a node and then verify that it was correctly saved in the database and displayed on the page.

First steps

If it doesn't exist yet, you need to create a file that contains the test cases. It is usually called module.test. Make sure to add a line to your module.info file so that it is picked up by Simpletest.

files[] = module.test

Then, create a class for your first test case. A few things are important:

  • Extend from DrupalWebTestCase
  • Implement the getInfo() method, see the example
  • Enable the necessary modules in the setUp() method, again, see the example below.
  • When naming your test case, prefix it with your module name and use TestCase as the suffix. Oh, and use camelCase.

An example:

<?php
/**
 * Administration tests for my module.
 */
class MyModuleAdministrationTestCase extends DrupalWebTestCase {

 
/**
   * Implements getInfo().
   */
 
public static function getInfo() {
    return array (
     
'name' => t('Administration'),
     
'description' => t('Administration tests for my module.'),
     
'group' => t('My module'),
    );
  }

  public function
setUp() {
   
// Enable any module that you will need in your tests.
   
parent::setUp('mymodule', 'anothermodule');
  }
}
?>

After saving that, you can hit the "Clean environment" button at admin/config/development/testing and your test case should show up below the group you've chosen. You can already run it, but since there are no test methods defined yet, Simpletest will report exactly that.

Write a test

You can follow these basic steps to create the initial test method. Of course, you can always change and extend this later, but it makes sense to start with this.

  1. Create a test method. You can call it whatever you want, just prefix it with test. An example:
    <?php
    public function testCreateContent() {
    }
    ?>

    Everything that follows now comes, needs to be placed in this (or another) test method.

  2. Next, think about the users you need, if any. Simpletest provides an easy way to create users and automatically assign a role with a given set of permissions to them. In this example, I am creating two users, one of them has some administration permissions and a normal user that just has the default permissions.
    <?php
    $admin
    = $this->drupalCreateUser(array('administer nodes', 'administration permission for my module'));
    $user = $this->drupalCreateUser();
    ?>

    Hint: To find out a permission name, go to the permission page at admin/people/permissions and check name of the checkbox. You can't use the displayed label, because that is often different than the name.

  3. Once created, there is another helper function that allows you to log in as a specific user. Note that you always have to use the user object returned by the drupalCreateUser() call, because it has the unencrypted password attached to it. Example:
    <?php
    $this
    ->drupalLogin($admin);

    // This does not work.
    $admin_new = user_load($admin->uid);
    $this->drupalLogin($admin_new);
    ?>
  4. Then, you have to actually write what you would do when you would do this test manually. Apart from calling functions directly, the two most common methods used for this are drupalGet() and drupalPost(). The first allows to GET a specific URL while the second allows to fill out a form and POST it. Two other commonly used functions are randomName() and randomString(). The first creates a alphanumeric string with the specified length and the second can contain any possible character, including special characters.
    An example:
    <?php
    // GET a page.
    $this->drupalGet('admin/config/category/mymodule');

    // POST a simple form.
    // $edit is an array that contains the form values. The key is the form element name and the value is, well, the value.
    // This example matches a form with two input elements (probably a textfield or textarea because we used a random string)
    // but you can also provide values for selects, checkboxes and other form elements.
    $edit = array(
     
    'name' => $this->randomName(8),
     
    'label' => $this->randomString(20),
    );
    // First argument is the URL, then the value array and the third is the label the button that should be "clicked".
    $this->drupalPost('admin/config/category/mymodule/add', $edit, t('Create'));

    // A third way to directly interact with the website is clickLink(), which allows to click a link by it's label
    // (content of the a tag). This extracts the href value and loads that.
    $this->clickLink(t('click me'));
    ?>

    Hint: drupalPost() first does a drupalGet() of the specified URL. If you already are on the page you want or are in the process of submitting a multi-step form, you can skip that step by passing NULL as the URL.

  5. The last thing to do are the assertions. The only things that are automatically reported by Simpletest as Exceptions are PHP errors and exceptions. To verify that the code actually did what it is supposed to, we have to add some assertions. There is a large number of helper functions for this available, see http://drupal.org/node/265828. A short example:
    <?php
    // Assuming that your module prints a drupal_set_message() confirmation after the form was submitted, we can verify that.
    // The first argument is the string that we are looking for on the current page and the second, optional one, is what is displayed in the Simpletest test report.
    $this->assertText(t('Thing created.'), t('Confirmation message displayed'));

    // However, this doesn't actually test if the content was correctly saved. So we use a database call to verify this.
    // Load the label that corresponds to the name that we used in the form submit above.
    $label = db_query('SELECT label FROM {mymodule_stuff} WHERE name = :name', array(':name' => $edit['name']))->fetchField();
    $this->assertEqual($label, $edit['label'], t('Stuff was saved in the database.'));
    ?>

    Hint: assertText() works on the resulting text, any HTML tags have been removed. To verify it together with the HTML, use assertRaw(). Additionally, when using placeholders together with assertText(), always use @placeholder instead of %placeholder because the <em> tag has been removed too.

From this point on, writing tests is mostly repeating the steps above. It all comes down to figuring out which permissions you need, what pages to load, what forms to submit and finally, how to assert that everything did work out correctly.

Some tips

  • Automated tests are never complete, there is always something that is not tested. So manual testing is necessary too, although you can focus on the things that are not automatically tested, which saves *a lot* of time.
  • Tests can be wrong too, make sure to verify that the test is correct when you have test failures. A typical example in Drupal is that tests rely on clean URL's being enabled. Because the test bots don't...
  • Code coverage tells you which parts of your code isn't tested. It does not tell you which part of your code is working correctly. Just because a code block is executed doesn't mean that it was actually tested with every possible input and that the output was correctly verified.
  • If you have written tests for a contributed module, enable it for automated testing: http://drupal.org/node/689990. That will automatically run all tests for every patch that you upload to the issue queue. This does not only save your time (running tests can take quite some time...) but also helps ensure that patches written by others don't break your tests.
  • For every test method you have, Simpletest will re-install the whole Drupal installation, enable all necessary modules, clear the cache and rebuild everything. That is a slow and complex process, so think about the number of test methods you create. One way to limit the number but still have the tests grouped into multiple function is have a single testMethod() that calls multiple other functions which are not prefixed with test. For example, verifyStuff(), verifyOtherStuff(). However, note that these then share the same environment, make sure that they neither conflict nor rely on each other. You can also extend your class from DrupalUnitTestCase, this will not install a site for every method but you are quite limited there.

Official documentation

There is a lot of documentation about writing automated tests available. Start here: http://drupal.org/simpletest

Rating: