Skip to main content

My PHPUnit configuration for my Drupal projects

Published on

The other week, I was asked about how I maintain my PHPUnit config file for my Drupal projects. When running Drupal's PHPUnit test suites, you typically copy and modify the distribution phpunit.xml.dist file which lives in the web/core subdirectory. There is just one problem. This directory is technically a vendor directory and is replaced during any updates to the drupal/core dependency of your Drupal project. Every minor version will cause your file to reset, or anytime you install dependencies if Drupal core is patched.

One thing I noticed is that since I first followed your tuts a while back, my phpunit.xml is gone.  I'm guessing it gets overwritten when core is updated.  Do you just keep a copy of the file and paste it in when that happens? Is there a better solution?

I didn't have time to answer in Slack, so now I'm writing a blog about a better solution. I'll also share some of my settings that I use – specifically for the database and running a local webserver for functional testing.

If you want the tl;dr, here's my XML file: https://gist.github.com/mglaman/0baf5ccd7b13b844de105286a04e43d3. If you want to learn about what gets changed and why, read on!

The Automated Testing documentation on Drupal.org says that you should copy the web/core/phpunit.xml.dist to web/core/phpunit.xml. From there you can modify environment variables that PHPUnit will initiate that the tests expect to exist – such as the database connection string and host to access Drupal from for functional tests. Another documented alternative is to just provide those environment variables directly when you run PHPUnit.

The former is simple, until you update the drupal/core package, as I mentioned earlier. The latter is a pain, unless you are running your tests from a bash script which sets the environment variables before the PHPUnit command is executed.

My approach is to take the phpiunit.xml.dist for Drupal and copy it into the root of my project. From there I modify any paths to make them relative to its new location.

Fixing file paths in the PHPUnit configuration file

Since every reader may not be familiar with all of the components in the PHPUnit configuration file, I wanted to walk through the pieces which need to be changed and why they exist.

Hint: all of the work is pretending web/core to existing path definitions.

The bootstrap file

The first line that needs to be changed is the root phpunit element for the XML file and its bootstrap attribute. This defines a bootstrap file that is executed before the tests are run and sets up autoloading. For Drupal, the tests/bootstrap.php file sets up the various test namespaces for Drupal core and contributed projects in the code base.

We need to change bootstrap="tests/bootstrap.php" to bootstrap="web/core/tests/bootstrap.php".

The test suite definitions

Next there is the testsuites element, which is an array of testsuite elements. A test suite is a way of organizing tests. Drupal uses it to break tests into Unit, Kernel, Functional, and FunctionalJavascript tests. Each have their own base classes. The test suite definition points to a file which sets up the test discovery in the namespaces provided during the bootstrap phase.

By default, the testsuites look like this:

  <testsuites>
    <testsuite name="unit">
      <file>./tests/TestSuites/UnitTestSuite.php</file>
    </testsuite>
    <testsuite name="kernel">
      <file>./tests/TestSuites/KernelTestSuite.php</file>
    </testsuite>
    <testsuite name="functional">
      <file>./tests/TestSuites/FunctionalTestSuite.php</file>
    </testsuite>
    <testsuite name="functional-javascript">
      <file>./tests/TestSuites/FunctionalJavascriptTestSuite.php</file>
    </testsuite>
    <testsuite name="build">
      <file>./tests/TestSuites/BuildTestSuite.php</file>
    </testsuite>
  </testsuites>

The path is relative to the fact the file is normally in the web/core directory. So we need to add web/core to the beginning of each path. Without these definitions, PHPUnit would not be able to detect and execute any of your tests.

  <testsuites>
    <testsuite name="unit">
      <file>./web/core/tests/TestSuites/UnitTestSuite.php</file>
    </testsuite>
    <testsuite name="kernel">
      <file>./web/core/tests/TestSuites/KernelTestSuite.php</file>
    </testsuite>
    <testsuite name="functional">
      <file>./web/core/tests/TestSuites/FunctionalTestSuite.php</file>
    </testsuite>
    <testsuite name="functional-javascript">
      <file>./web/core/tests/TestSuites/FunctionalJavascriptTestSuite.php</file>
    </testsuite>
    <testsuite name="build">
      <file>./web/core//tests/TestSuites/BuildTestSuite.php</file>
    </testsuite>
  </testsuites>

While it is not often, there is a chance a new test suite could be introduced and your PHPUnit configuration file becomes stale. Since Drupal 8 was released there have only been two test suites added. FunctionalJavascript tests were added pretty early in the life cycle, once a way to run PhantomJS (now Chromedriver via WebDriver) was possible. And, recently, Build tests were added when Drupal's default project setup went to Composer.

The filters

There is a filter element that defines files which should be explicitly configured when doing code coverage reporting. I have honestly never run Drupal core or a contributed module with a code coverage report. There is so much magicalness in a lot of Drupal and missing type hints that I don't bother. But, it has paths that we should update.

The final result will look like the following. It sets up directories that should be analyzed for code coverage and has PHPUnit exclude test directories for code coverage.

  <!-- Filter for coverage reports. -->
  <filter>
    <whitelist>
      <directory>./web/core/includes</directory>
      <directory>./web/core/lib</directory>
      <!-- Extensions can have their own test directories, so exclude those. -->
      <directory>./web/core/modules</directory>
      <exclude>
        <directory>./web/core/modules/*/src/Tests</directory>
        <directory>./web/core/modules/*/tests</directory>
      </exclude>
      <directory>./web/modules</directory>
      <exclude>
        <directory>./web/modules/*/src/Tests</directory>
        <directory>./web/modules/*/tests</directory>
        <directory>./web/modules/*/*/src/Tests</directory>
        <directory>./web/modules/*/*/tests</directory>
      </exclude>
      <directory>./web/sites</directory>
     </whitelist>
  </filter>

Note: as of PHPUnit 9 it looks like this is now replaced for the coverage element. At the time of writing, Drupal 9.1.0 has not started its alpha phase and only supports PHPUnit ^8.4.1. 

Configuring Drupal test environment variables

There are two main environment variables that must be configured in order to run Drupal's test suites (unless you only want to run Unit tests, but that won't get you far when testing Drupal.)

  • SIMPLETEST_DB: this is the connection string used to install the database for Kernel, Functional, and FunctionalJavascript tests.
  • SIMPLETEST_BASE_URL: Functional and FunctionalJavascript tests interact with a fully installed Drupal site, and this URL is how the test site should be accessed.

In the renaissance of local development stacks, I really like to make my testing environment simple. I use SQLite for my database and PHP's built-in web server

I use the following for my database connection string:

    <env name="SIMPLETEST_DB" value="sqlite://localhost/sites/default/files/.ht.sqlite"/>

No database service or software needed. It's still super fast.

I use the following for my base URL:

    <env name="SIMPLETEST_BASE_URL" value="http://127.0.0.1:8080"/>

From my project root I will run the built-in server on port 8080.

php -S 127.0.0.1:8080 -t web

I use the default WebDriver configuration for Chromedriver. I'll run the built-in server in one terminal window and Chromedriver in another.

🙌 Easy local tools for running the tests.

The finished configuration file

So, now, you should have a complete PHPUnit configuration file in the root of your Drupal project and start running all of those wonderful tests you're writing! Here is what it should look like:

<?xml version="1.0" encoding="UTF-8"?>

<phpunit bootstrap="web/core/tests/bootstrap.php" colors="true"
         beStrictAboutTestsThatDoNotTestAnything="true"
         beStrictAboutOutputDuringTests="true"
         beStrictAboutChangesToGlobalState="true"
         printerClass="\Drupal\Tests\Listeners\HtmlOutputPrinter">
  <php>
    <ini name="error_reporting" value="32767"/>
    <ini name="memory_limit" value="-1"/>
    <env name="SIMPLETEST_BASE_URL" value="http://127.0.0.1:8080"/>
    <env name="SIMPLETEST_DB" value="sqlite://localhost/sites/default/files/.ht.sqlite"/>
    <env name="BROWSERTEST_OUTPUT_DIRECTORY" value=""/>
    <env name="MINK_DRIVER_CLASS" value=''/>
    <env name="MINK_DRIVER_ARGS" value=''/>
    <env name="MINK_DRIVER_ARGS_PHANTOMJS" value=''/>
    <env name="MINK_DRIVER_ARGS_WEBDRIVER" value='["chrome", {"browserName":"chrome","chromeOptions":{"args":["--disable-gpu", "--no-sandbox", "--headless"]}}, "http://127.0.0.1:9515"]'/>
  </php>
  <testsuites>
    <testsuite name="unit">
      <file>./web/core/tests/TestSuites/UnitTestSuite.php</file>
    </testsuite>
    <testsuite name="kernel">
      <file>./web/core/tests/TestSuites/KernelTestSuite.php</file>
    </testsuite>
    <testsuite name="functional">
      <file>./web/core/tests/TestSuites/FunctionalTestSuite.php</file>
    </testsuite>
    <testsuite name="functional-javascript">
      <file>./web/core/tests/TestSuites/FunctionalJavascriptTestSuite.php</file>
    </testsuite>
    <testsuite name="build">
      <file>./web/core//tests/TestSuites/BuildTestSuite.php</file>
    </testsuite>
  </testsuites>
  <listeners>
    <listener class="\Drupal\Tests\Listeners\DrupalListener">
    </listener>
    <!-- The Symfony deprecation listener has to come after the Drupal listener -->
    <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
    </listener>
  </listeners>
  <!-- Filter for coverage reports. -->
  <filter>
    <whitelist>
      <directory>./web/core/includes</directory>
      <directory>./web/core/lib</directory>
      <!-- Extensions can have their own test directories, so exclude those. -->
      <directory>./web/core/modules</directory>
      <exclude>
        <directory>./web/core/modules/*/src/Tests</directory>
        <directory>./web/core/modules/*/tests</directory>
      </exclude>
      <directory>./web/modules</directory>
      <exclude>
        <directory>./web/modules/*/src/Tests</directory>
        <directory>./web/modules/*/tests</directory>
        <directory>./web/modules/*/*/src/Tests</directory>
        <directory>./web/modules/*/*/tests</directory>
      </exclude>
      <directory>./web/sites</directory>
     </whitelist>
  </filter>
</phpunit>

And a link to the XML file as a Gist: https://gist.github.com/mglaman/0baf5ccd7b13b844de105286a04e43d3&nbsp;

I'm available for one-on-one consulting calls – click here to book a meeting with me 🗓️