Break out of the SimpleTest sandbox

One of the common elements of every single successful web project in history is thorough testing, whether manual or automated. Thankfully Drupal has a relatively painless out-of-the-box testing framework however it’s biggest flaw is that it aims to do end-to-end testing using sandboxed environments – which for most projects just doesn’t make sense. Better yet there’s no documentation on how to get out of the sandbox.

So ... what’s the sandbox?

When a SimpleTest test is run in Drupal the following things happen:

  1. A random table prefix string is created.
  2. The database connection is cloned and a completely fresh and minimal version of Drupal is installed in the site default database using the random table prefix generated in step 1.
  3. Any modules that the current running test requires to be enabled are enabled.
  4. The test is run.

So what’s the problem? In the weak typed spaghetti code world of an average Drupal site full of alter hooks, preprocess functions, contributed modules and custom modules competing for the last word - testing a feature by isolating it and disregarding all other influences leads to coloured results. We want to know when something breaks on our particular site, in our particular use case.

The solution – extend SimpleTest!

To get started download the template module below – it contains a super-generic testing module template that you can hack, change, destroy or make better.

The common test class

The common test class will be responsible for providing all the common functions used across our site tests as well as overriding the default SimpleTest class to remove database sandboxing. In the example module the abstract common class is defined within the site_testing.common.test file.

Database sandboxing takes place in two protected functions – setUp and tearDown. setUp is executed at the beginning of every single test runner within a test class and is responsible for initiating variables for the test whereas tearDown is executed at the end of every single test runner within a test class. Removing sandboxing massively increases performance as we no longer have to re-install Drupal at the beginning of every single test runner.

/** * @file * Common testing class for this Drupal site. */ abstract class SiteTesting extends DrupalWebTestCase { /** * Overrides default set up handler to prevent database sand-boxing. */ protected function setUp() { // Use the test mail class instead of the default mail handler class. variable_set('mail_system', array('default-system' => 'TestingMailSystem')); $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); $this->public_files_directory = $this->originalFileDirectory; $this->private_files_directory = variable_get('file_private_path'); $this->temp_files_directory = file_directory_temp(); drupal_set_time_limit($this->timeLimit); $this->setup = TRUE; } /** * Overrides default tear down handler to prevent database sandbox deletion. */ protected function tearDown() { // In case a fatal error occurred that was not in the test process read the // log to pick up any fatal errors. simpletest_log_read($this->testId, $this->databasePrefix, get_class($this), TRUE); $emailCount = count(variable_get('drupal_test_email_collector', array())); if ($emailCount) { $message = format_plural($emailCount, '1 e-mail was sent during this test.', '@count e-mails were sent during this test.'); $this->pass($message, t('E-mail')); } // Close the CURL handler. $this->curlClose(); } }

Write your first test

Included in the example module is a test file called site_testing.homepage.test that provides some easy tests to get you going. One thing to note in this example is you can never pass a user loaded using user_load through the drupalLogin system as the user object expects to have a raw password (pass_raw) value to type into the password textfield. Instead we’re creating a user within the setUp function and then destroying it in the tearDown to make sure the test user isn’t left over after we’ve finished our tests.

/** * @file * Test for the home page of this website. */ class SiteTestingHomePageTest extends SiteTesting { /** * Stores the user created for this test. */ protected $siteUser; /** * Information about the home page test. */ public static function getInfo() { return array( 'name' => 'Home page', 'description' => 'Tests the contents of the home page.', 'group' => 'Site Testing', ); } /** * Create the site user to be used within the home page tests during setup. */ public function setUp() { // Run the default setUp function provided by our common testing class. parent::setUp(); // Create a user who has access content permission. $this->siteUser = $this->drupalCreateUser(array('access content')); } /** * Tests that logged in users do not get log in form. */ public function testHomePageLoginMarkup() { // Visit the home page. $this->drupalGet(''); // Check the welcome message and username field are present. $this->assertText('Welcome to Drupal', 'Title text found.'); $this->assertFieldByName('name', '', 'Username field found.'); // Log in as the site user we created in the setUp phase of the test. $this->drupalLogin($this->siteUser); // Visit the homepage and check the username field is not present. $this->drupalGet(''); $this->assertNoFieldByName('name', '', 'Username not visible for logged in user.'); } /** * Test that the home page has the 'Powered by Drupal' message. */ public function testStillAnon() { // Visit the home page. $this->drupalGet(''); // Check that the powered by block exists using XPATH. $powered_by_block = $this->xpath('//div[@id="block-system-powered-by"]'); $this->assertTrue($powered_by_block, 'Powered by block exists.'); // If the powered by block exists, check the powered by text exists. if ($powered_by_block) { $this->assertText('Powered by Drupal', 'Powered by message is correct.'); } } /** * Remove the user created in this test before running the final tear down. */ public function tearDown() { // Remove user created in the setUp part of this test. if ($this->siteUser) { user_delete($this->siteUser->uid); } // Run the default teardown function provided by our common testing class. parent::tearDown(); } }

Assertions!

Assertions are checks performed during a test to declare whether a piece of functionality performed correctly or not. The results of your assertions are displayed in the test report once your tests are finished. A massive list of assertions are available at https://drupal.org/node/265828.

// This will fail. $this->assertTrue('foo' == 'bar', 'The two strings are equal to eachother.'); // This will pass. $this->assertTrue('foo' == 'foo', 'The two strings are equal to eachother.');

Lets run those tests

To run tests, enable your testing module and visit /admin/config/development/testing. Open the Site Testing section and select the tests you’d like to run and click ‘Run tests’. You should get a sexy report with a lot of green rows.

The bad and the ugly

When writing tests outside of the sandbox and on your database make sure not to do anything destructive or messy. Any users or nodes you create should be cleaned up in tearDown or just accepted as collateral damage to your development database (in which case you should never run those tests on staging or live versions of your site).

Please write tests

The zip file attached to this article contains everything you need to get started writing your own custom site-specific tests – and it’s painfully easy! It’s a worthwhile investment even for the smallest of sites and saves time and stress in the long run. Writing tests is therapeutic, which may sound crazy but it provides you as a developer with so much more confidence in your code and the use cases you're covering and often allows you to find mistakes before code handover which makes you look like a more solid developer in the long run. The 10 – 20% investment in a basic automated testing routine will eliminate so much back-and-forth client-developer relation time and let you focus on what you actually enjoy, coding the hell out of your new project.