Third & GroveThird & Grove
Nov 1, 2016 - Brandon Barnes

Running a PHPUnit test for each site in a Drupal 7 multisite

Recently I ran into a requirement where I needed to run a PHPUnit test case against each site in a multisite Drupal 7 installation. While booting a single site in a unit test is fairly straightforward, booting multiple sites in the same test quickly ran into issues. The problem was that after the first site is bootstrapped, there were a lot of global and static values in memory that prevented Drupal from going "backwards" and starting the bootstrap process again. Luckily, PHPUnit gives us a few tools to work around that.

Some Goals:

  1. The test must be run with a single function - we can't have one function per site.
  2. If a single site fails, the rest of the sites should continue to be tested.
  3. A previous test should not pollute global or static values in memory for future tests.

PHPUnit features we will leverage:

  1. @runTestsInSeparateProcesses
    • Documentation
    • This ensures that no global or static Drupal information exists between 2 tests. Each test is run under it's own process, so it will make the test run slower overall.
  2. @preserveGlobalState disabled
    • Documentation
    • By default, PHPUnit attempts to restore any global data between tests - we want to override that behavior.
  3. Data Providers
    • Documentation
    • We will use a data provider to facilitate running the single test function against a possibly dynamic number of Drupal sites. A data provider is basically just a way of passing a set of data or arguments to a test function.

The Code

/**
 * Runs a test against each site in a Drupal 7 multisite install.
 *
 * @runTestsInSeparateProcesses
 * @preserveGlobalState disabled
 */
class TagMultisiteTest extends PHPUnit_Framework_TestCase {
 
  /**
   * Derives the boot info needed for each site defined in /sites/sites.php.
   *
   * This is the data provider function. This example dynamically builds the
   * boot info from the info available in sites.php, but you could just as 
   * easily have a static array of data.
   * 
   * @return array
   */
  public function provider() {
    // This project stores the multisite info in a global variable defined 
    // in sites.php.
    require_once getcwd() . '/sites/sites.php';
    global $TAG_SITES;
    $boot_info = array();
    $sites = $TAG_SITES;
 
    // The site formats follow the following pattern:
    // $sites['DOMAIN.com.COUNTRY_CODE.LANGUAGE_CODE'] = 'COUNTRY_CODE'
    // We will loop over a set of this data and derive the pieces that
    // are needed to boot into Drupal.
    foreach ($sites as $site => $country_code) {
      $exploded_site = explode('.' . $country_code, $site);
      $http_host = trim($exploded_site[0], '.');
      $language = $country_code;
      if (!empty($exploded_site[1])) {
        $language = trim($exploded_site[1], '.');
      }
 
      $script_name = "/{$country_code}/{$language}/index.php";
      $request_uri = "/{$country_code}/{$language}/";
 
      $boot_info[$country_code] = array(
        array(
          'http_host' => $http_host,
          'script_name' => $script_name,
          'request_uri' => $request_uri,
          'remote_addr' => '127.0.0.1',
          'language' => $language,
          'country_code' => $country_code,
        )
      );
    }
 
    return $boot_info;
  }
 
  /**
   * Tests that a variable exists for a set of sites.
   *
   * This is a our test function. We use the @dataProvider annotation to
   * to point at the provider() function defined above. The data returned
   * by the provider is given as a parameter in the test function.
   * 
   * This test is simplified from the original, but should give some
   * idea of what's possible.
   *
   * @dataProvider provider
   * @param array $boot_info
   */
  public function testVariableExists($boot_info) {
    // Boot into Drupal using the boot info given from the data provider.
    $_SERVER['HTTP_HOST'] = $boot_info['http_host'];
    $_SERVER['SCRIPT_NAME'] = $boot_info['script_name'];
    $_SERVER['REQUEST_URI'] = $boot_info['request_uri'];
    define('DRUPAL_ROOT', getcwd());
    require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
    $_SERVER['REMOTE_ADDR'] = $boot_info['remote_addr'];
    drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
 
    $empty_variable = variable_get('definitely_should_not_exist')
 
    $this->assertEmpty($empty_variable);
  }
}

 

Hopefully this should help you get started if you tasked with a similar set of requirements. Let me know if you run into problems or if you have any tips to make this more streamlined.

Bonus

Thanks to the data provider, you can also run this test for a single site in the multisite install instead of all of them. You do this by passing the --filter argument with the array key of the data. In this case we are keying by the country code. So, you could run PHPUnit for US like so:

phpunit path/to/your/test/TagMultisiteTest.php --filter '@us'