fromJune 2014
Article:

PHPUnit and Drupal

Starting with a Clean SUT
2

Robot has a bright idea Rejoice!

Drupal 8 ships with PHPUnit!

PHPUnit is the PHP industry standard testing framework, and with it comes the potential to make significant gains in the way we test Drupal, both core and contrib.

There’s a lot to be said about setting up, configuring, running and integrating PHPUnit (and how to do it for Drupal in particular), about which there are ample generic resources on the web. http://drupal.org/phpunit is a good starting point; it has crucial links and information, particularly to the PHPUnit manual, which will become your best friend.

So, instead of duplicating what’s already out there, I’m going to focus on some principles that will help you get the most value from PHPUnit on your Drupal 8 sites and contributed modules. Principles go well with a printed medium, anyway – no motive to copy and paste!

Test the Right Thing

It’s important to identify the type of testing you really need to do. PHPUnit is capable of a number of different types: behavioral, functional, and unit, at the least. With Drupal, I tend to use it only for unit and narrowly-scoped functional tests (Simpletest has more tools for conventional Drupal integration testing), and I prefer Behat for behavioral testing.

Regardless of the type of test you’re writing, the first step is to properly understand what you’re testing. In testing parlance, this is the “system under test” (SUT). Clearly identifying the SUT can be surprisingly elusive, especially for those unaccustomed to testing. It is worth taking the time to get it right; a clear, well-understood SUT is the foundation of not only good tests, but good test suites.

When contemplating a unit or functional SUT, I always have one question: “What is the code I’m testing solely responsible for?”

This is an especially important question in Drupal, where the indirection of hooks can make targeted testing difficult. For example, we are often inclined to “test” a form alter by ensuring that its modifications are visible in the final HTML output. Testing at that level makes the entire form system the SUT, much broader than a “sole responsibility” of your form alter. And we know the SUT to be this wide because of how other code can “indirectly” make your test fail: say, a hook_element_info_alter() that mucks about with the form elements you used.

This example isn’t entirely fair, as there isn’t a clearly better way to test form alters. But I think it illustrates well the sort of critical thinking one needs to apply, especially for Drupal. And while PHPUnit is powerful, it’s not magic; it still needs a clean SUT. Unlike Simpletest, though, PHPUnit has tooling that can take your clean SUT and deliver a lot more value. In particular, PHPUnit can assist with “feedback localization” – one of the big failings in our hypothetical form alter test.

Fast Feedback is Gold

“Feedback localization” is a fancy way of saying, “when this test breaks, how easy is it to figure out why?”

This property is a significant factor in your tests’ utility: test failures from indeterminate origins require investigation, which eats into dev time. In the worst cases, such tests can introduce the same risk and anxiety as ungrokkable spaghetti code in the main codebase.

Of course, before Drupal 8, Drupal wasn’t particularly amenable to the sort of granular unit testing that enables good feedback localization. But now, with the introduction of Symfony and a broad move towards object orientation, these goals become much easier: well-designed object oriented code tends to break its logic down into small methods with specific purposes. And those methods tend to be ideal units for testing.

PHPUnit provides two mechanisms that increase feedback localization:

  • @depends - indicates that a given test is dependent on another test.
  • @covers - specifies that a given test “covers” a segment of code. The formal use for this is code coverage metrics, but it has benefits for humans as well.

Unfortunately, these mechanisms aren’t used very widely in core yet, but that’s no reason not to use them yourself. Let’s look at an example demonstrating both:

<?php
 
use Drupal\Tests\UnitTestCase;
 
class MathStuff {
  public function multiply($a, $b) {
    return $a * $b;
  }
 
  public function square($v) {
    return $this->multiply($v, $v);
  }
}
 
class MathStuffTest extends UnitTestCase {

  /**
   * @covers MathStuff::multiply
   */
  public function testMultiply() {
    $math = new MathStuff();
    $this->assertEquals(42, $math->multiply(7, 6));
  }
 
  /**
   * @covers SomeClass::square
   * @depends testMultiply
   */
  public function testSquare() {
    $math = new MathStuff();
    $this->assertEquals(49, $math->square(7));
  }
}

Because ::testSquare() @depends on ::testMultiply(), PHPUnit guarantees that if ::testMultiply() fails, then ::testSquare() is simply skipped. The test results contain only one failure, so there’s no question about the logic that really needs to be addressed. That is significant – Drupal core developers have probably spent tens of thousands of hours trying to track down obscure test failures that arise from hidden or implicit dependencies in logic.

@covers doesn’t add much programmatically to all this. Again, its formal purpose is to tell PHPUnit how to instruct XDebug about which lines to mark as ‘covered’. The value here is more about keeping your tests honest: without a @covers statement, PHPUnit considers all executed code to be covered. So, ::testSquare() would cover ::multiply(). But that’s a lie; ::square() relies on ::multiply()’s contract, but if you take a “black box” testing perspective (where implementation is ignored, only inputs and outputs matter), it does not test a sufficiently exhaustive set of input permutations for ::multiply(). At the least, a set of non-equal operands should also be tested for ::multiply() to be truly covered.

Thus, we mark the test with a @covers statement - and that’s as useful to the system as it is to a human, because it sends a more explicit message to the reader about what the intention of the test writer is than the test method name alone.

This is just a small sample of the sorts of things to consider with testing, and with PHPUnit in particular. For example, there is also the “white/clear box” approach, which tends to make heavy use of PHPUnit’s excellent mock objects.

As you explore, I hope you’ll keep in mind that getting a test to pass is just a first step. If you ignore things like feedback localization, you may quickly find yourself in possession of an unwieldy, brittle test suite.

Image: ©iStockphoto.com/crisaseo

Comments

Great insight, thanks. Am definitely going to use these @covers and @depends annotations. I've been using PHPUnit for a while now and love it, but had no idea that was possible.

I really hope testing will get easier and MUCH faster with D8. It's such a shame testing in D7 and D6 is so freakishly slow. There are ways, of course, to write better tests for Drupal, regardless of version (I even wrote a series about it here: http://wadmiraal.net/lore/2014/07/22/write-testable-code-in-drupal-part-1/), but the standard practice of just writing WebTests discourages unit testing in Drupal, because of the huge investment in time and energy. The fact we now have Symfony and the HttpKernel might make it easier, but if we still need fully bootstrapped Drupal environments for full test coverage, we will continue to prevent projects from using best-practices like TDD and Continuous Integration.

I'll stop rambling now. Great article ;).

Is there a reason why you state @covers SomeClass::square instead of @covers MathStuff::square ?