
Using Simpletest with Drupal 6
I started using SimpleTest today so I thought I'd document the process as I went along for personal reference as much as anything else.
Why use SimpleTest?
For me the reasons were pretty straightforward:
- I had wanted to for ages, the cool kids were using it and I didn't want to get left behind!
- I was working on a project that involved making changes to the guts of a lot of un-documented code, I wanted to know if my re-factoring was breaking anything.
- Drupal uses SimpleTest, a contributed module for Drupal 6, in core for Drupal 7, I hope to contribute tests when I get slick ;)
In general, writing tests just makes sense, yes there's a learning curve and it takes time to write the tests in the first place but the piece of mind that comes from code coverage is golden. Q: How do you know that code you just wrote doesn't break some seemingly unrelated functionality? A: Tests.
So let's get started with tests! These instructions are specific to Drupal 6 and SimpleTest 6.x-2.9, the recommended version for Drupal 6 at the time of writing. The SimpleTest module can be found at http://drupal.org/project/simpletest
You read the INSTALL.txt right?
Once you've unpacked the module the first thing you'll need to do is look at the INSTALL.txt file, we all do that anyway right? If you don't you'll miss the instruction about applying the core patch supplied with SimpleTest.
To apply the core patch I ran these commands in Terminal, assuming you're in your Drupal root directory and SimpleTest is at sites/all/modules/simpletest:
patch -p 0 < sites/all/modules/simpletest/D6-core-simpletest.patch
Next job, enable SimpleTest as you would any other module at Administer -> Site Building -> Modules and start building tests!
Where do you put tests?
Assuming we're working on the incredibly thoughtfully named "my_module", tests should go in a file named 'my_module.test' inside your my_module directory.
I added a few lines to start with:
<?php
// $Id$
/**
* @file
* Tests for my_module.
*/
class MyModuleTestCase extends DrupalWebTestCase {
/**
* Implementation of getInfo().
*/
public static function getInfo() {
return array(
'name' => t('MyModule tests'),
'description' => t('Tests for my module.'),
'group' => t('MyModule'),
);
}
}
Wasdat?
SimpleTest is written using OO, what we're doing here is extending the class defined in drupal_web_test_case.php. The getInfo() function is your opportunity to return an array of basic information about your tests to the SimpleTest module.
After clearing the cache using the devel module or at Administer -> Site Configuration -> Performance I was able to see my test group listed on the Testing page at Administer -> Site Building -> Testing - reassuring eh?
In a world of its own, well almost!
This bit involves some mental gymnastics :)
Each test is executed in a freshly created Drupal instance.
Let me unpack that a bit, when you select some tests and hit that 'Run Tests' button, the tests you selected are executed one by one in isolation of each other. For each test, SimpleTest builds a fresh install of Drupal, it carries out the test then it tears down that install repeating the process over and over for each and every test.
A fresh install of Drupal means a fresh install of Drupal. It uses the same database of the site you're working on but prefixes the tables so as not to merrily blast away your data but now here's the thing:
In this 'fresh install of Drupal', the stuff that you have on the site is NOT available to your test. For stuff read users, nodes, terms... everything.
That seems a bit bonkers right? but it's not, think about it, you get a clean system, in your test you have the ability to enable the modules you need with setUp(), submit forms with drupalPost(), browse drupalGet() and read (sort of) assertRaw() and assertText(), create users drupalCreateUser(), create nodes drupalCreateNode(), delete nodes, delete user #1, go wild, drop tables, let your hair down, all in a consequence free environment!
Your friend, setUp()
So the setUp() function is the life-giver, it's responsible for setting up each instance of Drupal, it also kindly accepts a list of modules to install which you pass in like so:
<?php
// $Id$
/**
* @file
* Tests for my_module.
*/
class MyModuleTestCase extends DrupalWebTestCase {
/**
* Implementation of getInfo().
*/
public static function getInfo() {
return array(
'name' => t('MyModule tests'),
'description' => t('Tests for my module.'),
'group' => t('MyModule'),
);
}
/**
* Implementation of setUp().
*/
public function setUp() {
// Call setUp and install some modules.
parent::setUp('my_module', 'her_module', 'his_module', 'ladies_first');
}
}
The modules passed in will be enabled in the order in which they are passed in, handy to note that.
You can do all the rest of your setting up too, let's use the simple example of creating a user and logging in:
<?php
// $Id$
/**
* @file
* Tests for my_module.
*/
class MyModuleTestCase extends DrupalWebTestCase {
/**
* Implementation of getInfo().
*/
public static function getInfo() {
return array(
'name' => t('MyModule tests'),
'description' => t('Tests for my module.'),
'group' => t('MyModule'),
);
}
/**
* Implementation of setUp().
*/
public function setUp() {
// Call setUp and install some modules.
parent::setUp('my_module', 'her_module', 'his_module', 'ladies_first');
// Create a user with some basic permissions and log them in.
$account = $this->drupalCreateUser(array('access content', 'create page content', 'delete any page content'));
$this->drupalLogin($account);
}
}
The $account user is now logged in, that lucky user has the 'access content', 'create page content' and 'delete any page content' permissions.
Enough already, how do you write a test?
Steady, this is done by adding cunningly named functions to your class, functions prefixed with, you guessed it, 'test'.
Let's do another really simple task, let's create a node, then output its nid in the results:
<?php
// $Id$
/**
* @file
* Tests for my_module.
*/
class MyModuleTestCase extends DrupalWebTestCase {
/**
* Implementation of getInfo().
*/
public static function getInfo() {
return array(
'name' => t('MyModule tests'),
'description' => t('Tests for my module.'),
'group' => t('MyModule'),
);
}
/**
* Implementation of setUp().
*/
public function setUp() {
// Call setUp and install some modules.
parent::setUp('my_module', 'her_module', 'his_module', 'ladies_first');
// Create a user with some basic permissions and log them in.
$account = $this->drupalCreateUser(array('access content', 'create page content', 'delete any page content'));
$this->drupalLogin($account);
}
public function testSomeAspectOfMyModule() {
// Create a node.
$node = $this->drupalCreateNode(array('type' => 'page', 'title' => 'Tests reduce wrinkles', 'body' => 'Some content...'));
// Output the nid of the node as a pass.
$this->pass(t('We created a node and its nid is !nid', array('!nid' => $node->nid)), 'MyModule');
}
}
Let's go just a little further, let's go visit that node and see if the title text of the node can be seen on the page:
<?php
// $Id$
/**
* @file
* Tests for my_module.
*/
class MyModuleTestCase extends DrupalWebTestCase {
/**
* Implementation of getInfo().
*/
public static function getInfo() {
return array(
'name' => t('MyModule tests'),
'description' => t('Tests for my module.'),
'group' => t('MyModule'),
);
}
/**
* Implementation of setUp().
*/
public function setUp() {
// Call setUp and install some modules.
parent::setUp('my_module', 'her_module', 'his_module', 'ladies_first');
// Create a user with some basic permissions and log them in.
$account = $this->drupalCreateUser(array('access content', 'create page content', 'delete any page content'));
$this->drupalLogin($account);
}
public function testSomeAspectOfMyModule() {
// Create a node.
$node = $this->drupalCreateNode(array('type' => 'page', 'title' => 'Tests reduce wrinkles', 'body' => 'Some content...'));
// Output the nid of the node as a pass.
$this->pass(t('We created a node and its nid is !nid', array('!nid' => $node->nid)), 'MyModule');
// Is the title visible on the page we just created?
// First we've got to get to the page.
$this->drupalGet('node/' . $node->nid);
// Okay, now check for the node title text.
$this->assertText('Tests reduce wrinkles', t('The expected node title was found on the page, rejoice!'), 'MyModule');
}
}
Now let's delete the node.
<?php
// $Id$
/**
* @file
* Tests for my_module.
*/
class MyModuleTestCase extends DrupalWebTestCase {
/**
* Implementation of getInfo().
*/
public static function getInfo() {
return array(
'name' => t('MyModule tests'),
'description' => t('Tests for my module.'),
'group' => t('MyModule'),
);
}
/**
* Implementation of setUp().
*/
public function setUp() {
// Call setUp and install some modules.
parent::setUp('my_module', 'her_module', 'his_module', 'ladies_first');
// Create a user with some basic permissions and log them in.
$account = $this->drupalCreateUser(array('access content', 'create page content', 'delete any page content'));
$this->drupalLogin($account);
}
public function testSomeAspectOfMyModule() {
// Create a node.
$node = $this->drupalCreateNode(array('type' => 'page', 'title' => 'Tests reduce wrinkles', 'body' => 'Some content...'));
// Output the nid of the node as a pass.
$this->pass(t('We created a node and its nid is !nid', array('!nid' => $node->nid)), 'MyModule');
// Is the title visible on the page we just created?
// First we've got to get to the page.
$this->drupalGet('node/' . $node->nid);
// Okay, now check for the node title text.
$this->assertText('Tests reduce wrinkles', t('The expected node title was found on the page, rejoice!'), 'MyModule');
// Let's delete that pesky node.
$this->drupalGet('node/' . $node->nid . '/delete');
$this->assertRaw(t('Are you sure you want to delete %title?', array('%title' => $node->title)), t('Good good, this is the right page.'), 'MyModule');
// Do it!
$this->drupalPost('node/' . $node->nid . '/delete', array(), t('Delete'));
// Check we got the 'deleted' feedback message.
$this->assertRaw(t('Page %title has been deleted.', array('%title' => $node->title)), t('We got the message, the node was deleted.'), 'MyModule');
// Just for giggles, make sure we get a 'Page not found' where the node used to live.
$this->drupalGet('node/' . $node->nid);
$this->assertText(t('The requested page could not be found.'), t('Yup, the page is not found, that node is bye bye.'), 'MyModule');
}
}
Sometimes things get confusing and tests fail when you think they should pass. Let's face it, we're flying blind and it's not always clear what page we're on, in our example, if we didn't give our user permission to 'delete any page content' we'd have got some fails. Sometimes it not always so obvious, one handy thing you can do is grab the raw HTML of a page and either dump that to a file (recommended) or output it as a pass so that see what what the internal browser is seeing:
<?php
// Inside a test.
$this->drupalGet('node/' . $node->nid);
file_put_contents('/tmp/output.html', $this->drupalGetContent());
So there you have it, there's obviously a lot lot more you can do, these are simple examples but hopefully this'll remind me how to do this stuff and hopefully it's given you a good introduction on the mechanics of writing tests.
Open up the links below, open drupal_web_test_case.php, open existing tests and behold what wondrous things can be done!
For more information check out these resources:
SimpleTest Tutorial - http://drupal.org/node/395012
SimpleTest API Functions - http://drupal.org/node/265762
SimpleTest Assertions - http://drupal.org/node/265828
UPDATE: arianek over at Affinity Bridge has written a very good post about SimpleTest, I thoroughly recommend checking it out!
There's also some really useful information over at Tigerfish Interactive - http://tiger-fish.com/blog/beginning-simpletest-drupal6
tags: drupal, simpletest
things to do: add new comment





I design and develop web sites built with 





Thanks, I looked all over on
Posted by Anonymous on Mon, 05/04/2010 - 06:57.
Thanks,
I looked all over on how to register a new test suite. 'Clear Cache', is exactly what I needed.
This took me six hours to find.
Joe G
Ugly Web Development
things to do: reply