Writing PHPunit tests for your custom modules in Drupal 8
Integrating PHPunit into your custom modules is now even easier.
Background
I have been doing a bit of Drupal 8 development as of recent, and am loving the new changes, and entities everywhere. I am passionate about automated testing, and when I saw that integrating PHPunit into your custom modules is now even easier, I set out to see how this all worked.
Why is PHPunit important
There are are number of reasons why PHPunit is a great idea
- it forces you to write testable code in the first place, this means small classes, with methods that do a single thing
- it runs in only a few seconds, there is also no need to have a working Drupal install
- integrates with PHPStorm, allowing you run your tests from within the IDE
Step 1, set up your phpunit.xml.dist file
There is a file that comes included with Drupal 8 core, but by default it will not scan any sub-directories under /modules
(e.g. like the very common /modules/custom
). I stumbled across this question on stackoverflow. So you have a couple of options from here:
Option 1 - Create and use your own phpunit.xml.dist file
You can simply copy (and modify) Drupal 8 core’s phpunit.xml.dist file into git repo somewhere (perhaps outside the webroot), and use this file for all your custom module tests.
Option 2 - Patch Drupal 8 core
Another option (which is the option I took) was to apply a simple patch to Drupal core. There is an open issue on drupal.org to look at scanning all sub-directories for test files. At the time of writing it was uncertain whether this patch would be accepted by the community.
Step 2, write your tests
There are some general conventions you should use when writing your PHPunit tests:
- the suffix of the filename should be
Test.php
, e.g.MonthRangeTest.php
- the files should all reside in either the directory
/MY_MODULE/tests/src/Unit/
or a sub directory of that
More information on the requirements can be found on the drupal.org documentation.
Dataproviders
Data providers are pretty much the best thing to happen to automated testing. Instead of testing a single scenario, you can instead test a whole range of permutations in order to find those bugs. You start by declaring an annotation @dataProvider
for your test method:
<?php
/**
* @covers ::getMonthRange
* @dataProvider monthRangeDataProvider
*/
public function testGetMonthRange($expected_start, $expected_end, $month_offset, $now) {
// ... more code
}
You then declare a method monthRangeDataProvider
that returns an array of test cases (which are also arrays). The items in the data provider method are passed one at a time to the testing method, in the same order they are declared (so you can map them to friendly names).
<?php
/**
* Data provider for testGetMonthRange().
*
* @return array
* Nested arrays of values to check:
* - $expected_start
* - $expected_end
* - $month_offset
* - $now
*/
public function monthRangeDataProvider() {
return [
// Feb 29 @ noon.
[1454284800, 1456790399, 0, 1456747200],
// ... more tests follow
];
}
More information can be found in the phpunit documentation for data providers.
Testing for expected exceptions
Just as important as testing valid inputs, you should also test invalid inputs as well. This is easily achieved with @expectedException
annotations above your test method:
<?php
/**
* Tests that an end date that is before the start date produces an exception.
*
* @expectedException Exception
* @expectedExceptionMessage Start date must be before end date
*/
public function testGetWorkingDaysInRangeException() {
// ... more code in here
}
Step 3, enhance your test class with PHPunit metadata
You can annotate both the test class and the methods to provide additional information and metadata about your tests:
@covers
This is mainly used for PHPunit’s automated code coverage report, but I find it also very helpful for developers to up front state what method that are testing.
@coversDefaultClass
This is used at a class level, and saves you having to write rather lengthy @covers statement for all your testing methods, if they all test the same class.
@depends
If a certain test makes no sense to run unless a previous test passed, then you can add in a ‘depends’ annotation above the test method in question. You can depend on multiple other tests too. Note, that this does not change the execution order of the tests, they are still executed top to bottom.
@group or @author
You can think of adding a ‘group’ to a PHPunit class the same as tagging in. It is free tagging in that sense, and you can tag a single class with many tags. This should allow you to categorise your tests. @author is an alias of group, the idea being you can run all tests written by a particular developer.
More information can be found in the PHPunit documentation on annotations.
Step 4, run your test suite
This section assumes you have opted to use Drupal core’s phpunit.xml.dist file (modify the paths as appropriate if you are using a file in another location).
List groups (or tags)
cd core/
../vendor/bin/phpunit --list-groups
Run all tests that are tags with a particular group (or tag)
cd core/
../vendor/bin/phpunit --group tamdash
Example CLI output
$ ../vendor/bin/phpunit --group tamdash
PHPUnit 4.8.11 by Sebastian Bergmann and contributors.
...........
Time: 5.01 seconds, Memory: 144.25Mb
OK (11 tests, 18 assertions)
If you are using PHPStorm, spend a few minutes and set this up too.
Example output
So now there is no need to flip back to your terminal if you just want to quickly run a group of tests.
Conclusion
PHPunit is a great way to be able to run quick tests on isolated parts of your code. Tests often take less than 10 seconds to run, so developer feedback is near instant. It also forces your developers to write better more testible code from the get go. This can only be a good thing. Personally I am very excited to see PHPunit join Drupal 8, and cannot wait to see what people do with it.
Comments
There seems to be quite healthy debate on whether contrib or custom modules should ship with their own phpunit.xml.dist file or whether Drupal core’s file should cover both. I am keen to hear anyone’s thoughts on this. Also let me know if you have any contrib modules in the wild shipping their own phpunit.xml.dist files, and how you found that process.