mediatribe.net -- Drupal and Web Development

Notice: this post was last updated 4 years 50 weeks ago so it might be outdated. Please be cautious before implementing any of suggestions herein.

Automatic testing for your Drupal update and upgrade paths

Introduction

Simpletest is great for unit and functional tests for your Drupal modules; you can also use Simpletest to automatically test your module updates and upgrade path. The process is similar in Drupal 7 and Drupal 8, and this article will show you how to do it, and include some simple example modules.

Terminology

First, some terminology: updates of your module remains within the same major Drupal version, for example you might be updating your module from 7.x-1.0 to 7.x-1.1, or from 7.x-1.1 to 7.x-2.0. Upgrades are from one major Drupal version to another, for example from 7.x-2.0 to 8.x-2.0.

Upgrades

Let's take as an example a module which displays a string when visiting the path "hello" of your website; and let's call the module "Upgrade Test". Let's say that:

  • In Drupal 6 the string is stored in a specific table in the database
  • In Drupal 7 it is stored in the variable system
  • In Drupal 8 it is stored in the configuration management system

You can download these simple modules from my github account, with functional and unit tests for each, but without the upgrade path tests (for now):

Based on these modules, we'll walk through the process of testing the upgrade path. Remember, there is no upgrade path between the above versions.

Let's start by creating a version 7.x-2.x which will contain one new feature: an upgrade path from 6.x-1.0. (You can follow along by downloading upgradetest-7.x-2.0.tar.gz from github.)

This will take the form of an implementation of hook_update_N() which will do the following:

  • Take the value our module has stored in the Drupal 6 database table, and place it in a variable for Drupal 7
  • Delete the Drupal 6 schema

This, of course, is a pain to test manually, so we'll need to do some automatic testing. Drupal 7 ships with a complete basic Drupal 6 database description (at modules/simpletest/tests/upgrade), which we'll use to simulate a Drupal 6 (within our D7 test), then add our schema from the Drupal 6 version of our module, with some dummy data. Then we'll run the update, and finally we'll make sure that everything works in D7 (the data from D6 appears correctly, and the D6 table has been deleted).

(We could also simply forego updating the entire D6 Drupal database to D7, and simply test our own database table. In fact, this is exactly what we'll do later on when testing updates (as opposed to upgrades).)

Let's start by creating a new file called "drupal-6.upgradetest.database.php" in our module's folder. Let's keep it empty for now. It will be used to fill a database to simulate how our module's data looks in a D6 database.

Now let's create a file called "upgrade.upgradetest.test" in our upgradetest folder, and referencing it in the .info file. In this file we'll define a new subclass of UpgradePathTestCase -- at the end we'll want it to look something like the upgrade path test case for the core poll module. In the setUp() method of this class, we'll populate the database ourselves (rather than with the installer, which is how functional tests work):

<?php
...
$this->databaseDumpFiles = array(
 
drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.filled.database.php',
 
drupal_get_path('module', 'upgradetest') . '/drupal-6.upgradetest.database.php',
);
parent::setUp();
...
?>

Now we need to fill the file "drupal-6.upgradetest.database.php". We need to simulate what a Drupal 6 database might look like for our module, along with some dummy data:

<?php
// install the schema for our module as it exists in the .install file of the
// D6 version of our module
db_create_table('upgradetest', array(
 
'description' => 'Stores only one line, the value displayed on the page "hello". Please see the README.txt of this module.',
 
'fields' => array(
   
// utid stands for "upgrade test id". If you are storing something else,
    // like beach balls, you might call your primary field bbid.
   
'utid'  => array(
     
'type' => 'serial',
     
'unsigned' => TRUE,
     
'not null' => TRUE,
     
'description' => 'Primary Key id (there should be only one).',
    ),
   
'name'  => array(
     
'type' => 'varchar',
     
'length' => 255,
     
'not null' => TRUE,
     
'description' => 'The value.',
    ),
  ),
 
'primary key' => array('utid'),
));

db_insert('upgradetest')->fields(array(
 
'name',
))
->
values(array(
 
'name' => 'simulating data entered in D6',
))
->
execute();
?>

At this point we must simulate that our module (upgradetest) is already enabled, to avoid having Drupal perform the module's new installation procedure. To do this we need to add an entry to the system table telling Drupal that upgrade path is already enabled, but that the schema version is 0, meaning that all update hooks need to be called when upgrading. Let's craft some PHP code with the result, and add it to our drupal-6.upgradetest.database.php file:

<?php
// tell drupal that the module upgradetest is active. This will prevent its
// normal installation, so we can test the upgrade path instead.

db_insert('system')->fields(array(
 
'name',
 
'type',
 
'status',
 
'schema_version',
))
->
values(array(
 
'name' => 'upgradetest',
 
'type' => 'module',
 
'status' => '1',
 
'schema_version' => '0',
))
->
execute();
?>

Now we can add a test case to our test class in drupal-6.upgradetest.database.php. What we need to test is that our data (in this case "simulating data entered in D6") appears in D7 after the upgrade.

<?php
public function testupgradetestUpgrade() {
 
$this->assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.'));

 
$this->drupalGet('hello');
 
$this->assertText('simulating data entered in D6', 'data entered in D6 appears in D7 after the upgrade');

 
$this->assertFalse(db_table_exists('upgradetest'), 'the old upgradetest has been deleted from the db');
}
?>

Now that we have written our test, you can write the actual upgrade hook as you normally would, and test it. See upgradetest-7.x-2.0.tar.gz from github for the result.

The concept is really very similar when testing the D7-to-D8 site, but we can't use variable_get() to get the variable (because variable_get() no longer exists in D8). Rather we need to query the database directly. The fully functional D8 upgrade test file can be found here.

Updates

To test updates, we won't use a special class. Rather, we will do the following:

  • setUp() your test class as you normally would, but don't install your module just yet.
  • Fiddle with the database's system table to simulate the module being already installed and enabled.
  • Tweak the database so as to simulate the minor version of your module you want to test.
  • Call your update hooks
  • Confirm that all went well.

Let's look at a very simple example: Let's say our 7.x-2.0 is using the variable upgradetest_value (which it is). Let's make 7.x-2.1 use the variable upgradetest_value1 instead.

Not very realistic, but it's just to get my point accross.

To see how I tested this please download upgradetest-7.x-2.1.tar.gz from github.

Bonus exercices

Astute readers will have noticed that the upgrade path from 7.x-2.0 to 8.x-2.0 is working fine (and is tested). However, trying to go from 7.x-2.1 to 8.x-2.0 will result in a broken upgrade path. I'll leave it to the reader to figure out how to fix (and test!) this.

It might also be interesting to create an updated 7.x-2.2 version with changes to the database, include a test, and make sure 8.x-2.x can be upgrade (and the upgrade tested) regardless of which version of 7.x is used.

Conclusion

Because I have not found any standard way of doing this in my numerous Google searches, the technique described herein may not be optimal -- I welcome any suggestions and discussion to make it better.