Third & GroveThird & Grove
Jun 22, 2017 - Brandon Barnes

Dynamically Generating PHPUnit XML Configuration

Scenario: You are working on a large project where the codebase is used for multiple install types, but each install type only uses certain sections of the codebase. Depending on the install, some of these install types might have overlapping code. For example, a multisite Drupal install where each site uses a different overlapping set of custom modules or maybe your project offers both a commerce and non-ecommerce option to your customers and you there is a shared "core" of code that applies to both.

For this type of project, when writing your unit tests, it helps to identify which tests are relevant to which install type by using the `@group` annotation provided by PHPUnit. To ensure coverage is reported accurately it helps to create a separate PHPUnit XML configuration for each group that specifies which sections of your project should be ignored when running tests for that group.

Depending on the size of your project and the number of groups, the PHPUnit XML configuration files will eventually have lots of duplicate elements. One way of cleaning this up is to build your configuration dynamically before PHPUnit is run.

For this very simplified example, let's go with the ecommerce vs. non-ecommerce scenario mentioned earlier. This project has 2 PHPUnit groups defined: `@store`, which is placed on tests specific to the ecommerce code, and `@info` which is placed on tests specific to the non-ecommerce code.

You will have 5 files:

  1. core.xml - The core PHPUnit XML configuration that applies to any tests run on this project.
  2. store.xml - XML configuration specific to the store group.
  3. info.xml - XML configuration specific to the info group.
  4. config-builder.php - This is where your dynamic config XML will get built.
  5. run-tests.sh - This is what you will run when you run your tests.

Here is a very simplified example of what the core.xml might look like. It defines the directory where the tests live and create a few exclusions for files that shouldn't be ever be included in the code coverage report.

<phpunit>
 
  <testsuites>
    <testsuite name="Acme Solutions">
      <directory>../path/to/your/project/code</directory>
    </testsuite>
  </testsuites>
 
  <filter>
    <whitelist processUncoveredFilesFromWhitelist="true">
      <directory  suffix=".inc">../path/to/your/project/code</directory>
      <directory  suffix=".module">../path/to/your/project/code</directory>
      <exclude>
        <directory suffix=".html">../path/to/your/project/code</directory>
        <directory suffix=".info">../path/to/your/project/code</directory>
      </exclude>
    </whitelist>
  </filter>
</phpunit>

 

For the store.xml, we only need to specify the elements specific for the @store code coverage report. In this example, we don't want the @info specific code to be included.

<phpunit>
  <filter>
    <whitelist processUncoveredFilesFromWhitelist="true">
      <exclude>
        <directory suffix=".inc">../path/to/your/project/code/info_module</directory>
        <directory suffix=".php">../path/to/your/project/code/info_module</directory>
      </exclude>
    </whitelist>
  </filter>
</phpunit>

 

And then the same idea for the info.xml, but in reverse.

<phpunit>
  <filter>
    <whitelist processUncoveredFilesFromWhitelist="true">
      <exclude>
        <directory suffix=".inc">../path/to/your/project/code/store_module</directory>
        <directory suffix=".php">../path/to/your/project/code/store_module</directory>
      </exclude>
    </whitelist>
  </filter>
</phpunit>

 

Then we create the config_builder.php file which will merge our config xml's and create a generated one that we will specify for PHPUnit. In this example, we are only merging the `exclude` elements, but this can easily be expanded to other elements.

<?php
 
// This gets passed from the run-tests.sh.
$group = getenv('PHPUNIT_CURRENT_GROUP');
 
// Load the core config that applies to all configs.
$doc1 = new DOMDocument();
$doc1->load(realpath(dirname(__FILE__)) . '/core.xml');
 
// Load the specific group that is currently being run.
$doc2 = new DOMDocument();
$doc2->load(realpath(dirname(__FILE__)) . '/' . $group . '.xml');
 
// Merge all children nodes under exclude node.
$exclude1 = $doc1->getElementsByTagName('exclude')->item(0);
$exclude2 = $doc2->getElementsByTagName('exclude')->item(0);
for ($i = 0; $i < $exclude2->childNodes->length; $i++) {
  $item2 = $exclude2->childNodes->item($i);
  $item1 = $doc1->importNode($item2, true);
  $exclude1->appendChild($item1);
}
 
// Reload and save generated file.
$generated = $doc1->saveXML();
$doc3 = new DOMDocument();
$doc3->loadXML($generated);
$doc3->save(realpath(dirname(__FILE__)) . 'generated.xml');

 

In run-tests.sh we will setup our test configuration and then run PHPUnit. It accepts one parameter which is the group name.

#!/bin/bash
 
# Second argument is test configuration and is required.
if [ -z "$1" ]; then
  echo 'Please provide a test configuration group name.'
  exit
fi
 
# Set the group as a temporary environment variable so that other code has access to it.
export PHPUNIT_CURRENT_GROUP=${1}
 
# Build the config xml dynamically based on group.
php config_builder.php
 
# Run PHPUnit.
phpunit --configuration generated.xml --coverage-html ${1} --group $1