Large software projects lead to many features being developed over time. Building new features on top of an existing system always risks regressions and bugs. One of the best ways to ensure that you catch those before your code hits production is by adding unit tests.
In this article series I will guide you through adding PHPUnit tests to your Drupal 7 modules. This article explores what unit tests are and why they are useful. And then looks at how to structure your Drupal 7 code in a way that is conducive to writing unit tests later on.
TL;DR
I encourage you to start testing your code. Here are the most important points of the article:
- Start writing object-oriented code for Drupal 7 today. In your hooks you can just create the appropriate object and call the method for that hook.
- Fake your methods to remove unmet dependencies.
- Leverage PHPUnit to ease writing tests.
Unit tests to the rescue
In Drupal 7 core, Simpletest was added so everyone could write tests for their modules. While this has been a great step forward, executing those integration tests is very slow. This is a problem when you want to do Test Driven Development, or if you want to run those tests often as part of your workflow.
A part of Simpletest is a way to write unit tests instead of integration tests. You just need to create your test class by inheriting from DrupalUnitTestCase
, but its drawback is that most of Drupal isn’t available. Most Drupal code needs a bootstrap, and it’s very difficult to test your code without a full (slow) Drupal installation being available. Since you don’t have a database available, you can’t call common functions like node_load()
or variable_get()
. In fact, you should think about your unit tests as standalone scripts that can execute chunks of code. You will see in the next section how PHPUnit can help you to create these testing scripts.
PHPUnit has you covered
In the greater PHP community, one of the leading unit test frameworks is PHPUnit, by Sebastian Bergmann and contributors. This framework is widely used in the community, attracting many integrations and extra features, compared to the aforementioned DrupalUnitTestCase
.
Daniel Wehner comments on these integrations saying:
Since http://drupal.org/node/1567500 Drupal 8 started to use PHPUnit as it's unit test framework. One advantage of PHPUnit is that there are tools around which support it already.
Here is a screenshot of PhpStorm where you can see how you can execute your tests from the IDE:
PHPUnit is the PHP version of the xUnit testing framework family. Therefore, by learning it, you will be able to leverage that knowledge in other languages. Besides there’s a big documentation and support base for xUnit architectures.
The best part of PHPUnit is that it allows you to write easy-to-read test classes efficiently, and it has many best practices and helper tools –like the test harness XML utility–. PHPUnit also comes with some handy tools to mock your objects and other developer experience improvements to help you write your tests in a clearer and more efficient way.
To use PHPUnit with Drupal 7 you need to write object-oriented code. The next section will show you an example of the dependency problem, and one way to solve it using OOP with the fake object strategy.
A change in your mindset
The hardest part of unit testing your Drupal code is changing your mindset. Many developers are getting ready to use object oriented PHP for Drupal 8, but they keep writing procedural code in their current work. The fact that Drupal 7 core is not as object oriented as it might have been does not imply that custom code you write must also be procedural and untestable.
In order to start unit testing your code, you need to start coding using OOP principles. Only loosely coupled code can be easily tested. Usually this starts by having small classes with clearly defined responsibilities. This way, you can create more complex objects that interact with those small pieces. Done correctly, this allows you to write unit tests for the simple classes and have those simple classes mocked to test the complex ones.
Unit testing is all about testing small and isolated parts of the code. You shouldn’t need to interact with the rest of the codebase or any elements in your system such as the database. Instead, all the code dependencies should be resolved through the use of mock objects, fake classes, dummies, stubs or test doubles.
Mock objects avoid dependencies by getting called instead of the real domain objects. See the Guzzle’s documentation for an example.
Gerard Meszaros writes about test doubles in these terms:
Sometimes it is just plain hard to test the system under test (SUT) because it depends on other components that cannot be used in the test environment. This could be because they aren't available, they will not return the results needed for the test or because executing them would have undesirable side effects. In other cases, our test strategy requires us to have more control or visibility of the internal behavior of the SUT.
When we are writing a test in which we cannot (or chose not to) use a real depended-on component (DOC), we can replace it with a Test Double. The Test Double doesn't have to behave exactly like the real DOC; it merely has to provide the same API as the real one so that the SUT thinks it is the real one!
In typical Drupal 7 modules, which is our System Under Test (SUT), there are many parts of the code that we want to test that rely on external dependencies –our depended-on component (DOC). Good examples of those dependencies are Drupal core, other contributed modules, or remote web services. The fact that a method calls a Drupal function, such as cache_get()
, makes it very difficult for the test runner to execute that code, since that function will not be defined during the test. Even if we manually included includes/cache.inc
, the cache_get()
function might require other include files or even an active database connection.
Consider the following custom class:
class MyClass implements MyClassInterface {
// ...
public function myMethod() {
$cache = cache_get('cache_key');
// Here starts the logic we want to test.
// ...
}
// ...
}
When we call myMethod()
we will need to have the database ready, because it is calling to cache_get()
.
// Called from some hook.
$object = new MyClass();
$object->myMethod();
Therefore, myMethod()
, or any code that uses it, is not unit testable. To fix this, we wrap cache_get()
in a class. The big advantage of this is that we now have a CacheController
object that deals with all of our cache needs by interacting with the Drupal API.
class CacheController implements CacheControllerInterface {
/**
* Wraps calls to cache_get.
*
* @param string $cid
* The cache ID of the data to retrieve.
* @param string $bin
* The cache bin to store the data in.
*
* @return mixed
* The cache object or FALSE on failure.
*/
public function cacheGet($cid, $bin = 'cache') {
return cache_get($cid, $bin);
}
}
And the custom class becomes:
class MyClass implements MyClassInterface {
// ...
public function __construct(CacheControllerInterface $cache_controller) {
$this->cacheController = $cache_controller;
}
// ...
public function myMethod() {
$cache = $this->cacheController->cacheGet('cache_key');
// Here starts the logic we want to test.
// ...
}
// ...
}
The calling code stays the same.
Our test class will execute myMethod()
with a fake cache controller that doesn’t need the bootstrap or the database.
// Called from the PHPUnit test case class.
$cache_controller_fake = new CacheControllerFake();
$object = new MyClass($cache_controller_fake);
$object->myMethod();
What our fake cache controller looks like:
class CacheControllerFake implements CacheControllerInterface {
/**
* Cache array that doesn't need the database.
*
* @var array
*/
protected $cache = array();
/**
* Wraps calls to cache_get.
*
* @param string $cid
* The cache ID of the data to retrieve.
* @param string $bin
* The cache bin to store the data in.
*
* @return mixed
* The cache object or FALSE on failure.
*/
public function cacheGet($cid, $bin = 'cache') {
return isset($this->cache[$bin][$cid]) ? $this->cache[$bin][$cid] : NULL;
}
}
The key is that the test will create a fake object for our CacheController
and pass it to the SUT. Remember that you are not testing cache_get()
but how the code that depends on it is working.
In this example, we have removed the dependency on includes/cache.inc
and the existence of the database to test a method that calls to cache_get()
. Using similar techniques you can test all your classes in your module.
The next article of the series will get deeper into the matter by covering:
- Mocking tools like: PHPUnit mocking objects and the Mockery project.
- Dependency injection in Drupal 7 to pass your dependencies easily.
- Drupal Unit Autoload to reduce the number of classes to mock.
- A real life example that applies all these concepts.
Do you add unit tests to your Drupal 7 modules? Share your experience in the comments!