Writing loosely-coupled code is the path to great DX in Drupal 8

[This post brings together some of the ideas presented in my DrupalCon Portland talk on Dependency Injection and ideas presented in Mark Sonnabaum's DrupalCon Prague talk on writing unit-testable code]

D8 now comes with PHPUnit, a unit testing framework for PHP. Many tests in core are being converted to unit tests: at the time of writing there are 845 tests making 1992 assertions and it takes roughly 6 seconds to run the whole lot. That is seriously fast compared to web tests. But this speed isn't the only great thing about unit testing. In this post I hope to explain why having a great unit testing framework in Drupal core and the implications of writing unit-testable code are what make me most excited about Drupal 8.

The purpose of a unit test is to verify the behavior of a unit of code in isolation. Because the tests are run outside of the normal application context of your code, the code cannot have hidden dependencies on things like database connections or functions that will not be loaded in the test environment. Most Drupal 7 code has exactly these kinds of hidden dependencies. Think of a function that makes a call to module_implements() - it expects that function to exist, and that function expects the db_query() function to exist, which in turn expects a database connection.

In Object Oriented Programming, you could sometimes write classes with problematic dependencies if you are not careful, making them hard to test. Suppose you have a class that requires an object of some other class in order to do its job. Maybe it instantiates that other object in its constructor:

class myClass {
  function __construct() {
    $this->myDependency = new SomeDatabaseThing('some_connection_info');
  }
  function getDataFromDep() {
    $data = $this->myDependency->getData();
    return $data;
  }
}

How can you test how the class interacts with that SomeDatabaseThing? As Steve Freeman and Nat Pryce put it in Growing Object-Oriented Software Guided by Tests,

…for a class to be easy to unit-test, the class must have explicit dependencies that can easily be substituted and clear responsibilities that can easily be invoked and verified.

If there are systems that your code normally collaborates with (e.g. database connections, file systems, other modules, other classes), it must be possible to provide a substitute for those collaborators, as the test environment will be completely ignorant of the usual application context.

Contrast this with the web tests we get with the simpletest module. They recreate exactly the same environment that your code normally runs in, so hidden dependencies are never exposed. And of course these tests are incredibly slow to run as well, because of all this setup that they do.

But the worst thing about code with these kinds of hidden dependencies is not that it is untestable - it's that it is far too tightly coupled to other systems. This makes it difficult to reuse in other contexts, and difficult to maintain because changes in the systems it depends on may necessitate corresponding changes to the code itself.

So, what can be done about such dependencies? We obviously can't eliminate them and still have classes that have clear and verifiable responsibilities. But we can change the nature of our code's relationship to its dependencies. In OOP, we can use a technique called Dependency Injection.

Dependency Injection, despite the scary name, is an incredibly simple idea. In its most basic and common form, it is just about passing an object its instance variables as constructor parameters. E.g.:

class MyClass {
  function __construct(DatabaseThingInterface $db_thing) {
    $this->myDependency = $db_thing;
  }
  function getDataFromDep() {
    $data = $this->myDependency->getData();
    return $data;
  }
}

The class is declaratively expressing its dependency rather than instantiating it in its own constructor. For all its simplicity, this is a very powerful technique. Our class now no longer needs to know anything about how to construct the thing it needs. All it needs to know are which methods it can call on it, and that is exactly what the interface provides. Having this minimal level of knowledge about its collaborators makes a class much more reusable. The above class can now be used with anything that implements DatabaseThingInterface.

And as a happy side-effect of this decoupling… Oh look! This class is now testable too. We can now write a unit test that passes in some kind of "test double" -- a mock, stub or fake object -- that implements the DatabaseThingInterface and test how our class interacts with it.

Here's how you might use a mock in PHPUnit to test the getDataFromDep() method.

class MyClassTest extends UnitTestCase {

  public function testMyClass() {
    $mock_dependency = $this->getMock('DatabaseThingInterface');
    $mock_dependency
      ->expects($this->once())
      ->method('getData');
    $my_object = new MyClass($mock_dependency);
    $my_object->getDataFromDep();
  }
}

What this test does is assert that the getData() method will be called exactly once on the passed in dependency when the getDataFromDep() method is called. It is asserting on behavior rather than on state - that is the nature of mocks: you set up expectations about when methods will be called on your mock objects, and the expectation being met is essentially the assertion. This test would fail if the getData() method had not been called or if it had been called more than once.

Stubs, Fakes and Mocks oh my!

So we've seen a basic example of what a mock is, what about all these other types of test doubles? We'll look at an example of a stub in a moment, but here are Martin Fowler's definitions of the various types types of test doubles (from http://martinfowler.com/articles/mocksArentStubs.html):

Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an in memory databaseis a good example).
Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. Stubs may also record information about calls, such as an email gateway stub that remembers the messages it 'sent', or maybe only how many messages it 'sent'.
Mocks are […] objects pre-programmed with expectations which form a specification of the calls they are expected to receive.

At this point you may be wondering how all of this is going to lead to a great developer experience, so let's have a look at a real example. There's a contributed module called subpathauto, which extends core's path module by matching on subpaths of aliases. As explained on the project page, "For example, if user/1 is aliased to users/admin, this module rewrites the link to the user contact page user/1/contact to use the aliased URL users/admin/contact instead." I had a go at porting a part of its functionality to Drupal 8.

The heart of the subpathauto module is a function that does roughly the following in D7 to resolve the subpath of an alias:

if ($processed_path = drupal_lookup_path('source', $base_path, $path_language)) {
  if ($processed_path !== $path) {
    $path = $processed_path . '/' . implode('/', $subpath);
    return $path;
  }
}

Notice the call to drupal_lookup_path(). That function in turn has the following dependencies:

  • The $language_url global variable
  • drupal_static
  • variable_get
  • drupal_path_alias_whitelist_rebuild
  • current_path
  • cache_get
  • LANGUAGE_NONE
  • db_query

Unsurprisingly, testing the subpathauto functionality requires jumping through some hoops. The D7 tests need to save actual path aliases to the test db in order to confirm that the subpath look-up behaves as it should.

In Drupal 8, the above functionality can be achieved with a class that expresses a dependency on something implementing the InboundPathProcessorInterface. It uses this thing to see if it can get a match on the path after removing portions of it. (What will actually get injected is the core class responsible for alias lookups, but the SubPathAuto class doesn't need to know that.)

class SubPathAuto {
  public function __construct(InboundPathProcessorInterface $path_processor) {
    $this->pathProcessor = $path_processor;
  }
}

And then in the method responsible for the lookup:
$processed_path = $this->pathProcessor->processInbound($path, $request);
if ($processed_path !== $path) {
  $path = $processed_path . '/' . implode('/', array_reverse($subpath));
  return $path;
}

Here's how this functionality could be tested:
public function testInboundSubPath() {
  $alias_processor = $this->getMock('Drupal\Core\PathProcessor\InboundPathProcessorInterface');
  $alias_processor->expects($this->any())
    ->method('processInbound')
    ->with($this->equalTo('content/first-node'), $this->anything())
    ->will($this->returnValue('node/1'));
  $subpathauto = new SubPathAuto($alias_processor);

  // Look up a subpath of the 'content/first-node' alias.
  $processed = $subpathauto->lookup('content/first-node/a', Request::create('content/first-node/a'));
  $this->assertEquals('node/1/a', $processed);
}

Although we are using the getMock() method again here, we are in fact creating a stub: an object we've programmed to provide a response of "node/1" if its processInbound() method gets called at any time with 'content/first-node' as the first parameter (and anything at all as the second). We then inject that into our SubPathAuto class and test that it correctly resolves a subpath of the programmed alias.

By far the biggest advantage of the D8 version is that it is completely decoupled from the path alias system. It doesn't care a hoot how the path aliasing system works - it can be used with *any* implementation of PathProcessor, not just one that happens to look up path aliases in a database. The test we have written for it is proof of this.

So writing unit tests, as well as being a great way to verify the correct behavior of your classes, is also the first time you get to prove their reusability.

The key to achieving this is coupling to abstractions (interfaces) rather than concretions (classes) so that your code knows as little as possible about its collaborators. It may seem counterintuitive, but writing more "ignorant" code is what leads to a great developer experience.

Comments

Posted on by Ryan Aslett.

In the above example you mention that the SubPathAuto class 'can be used with *any* implementation of PathProcessor' - who gains that flexibility? Does it only give the maintainer of the SubPathAuto module the flexibility to swap out implementations? Or is there some standard methodology (i.e. a hook) such that any module could override which implementation is injected when the constructor is called?

I see this as a huge DX improvement for core/contrib maintainers, but does this provide site implementing developers more means to override behavior?

Posted on by Katherine.Bailey.

Hi Ryan,
that is a great question. So, one area I didn't touch on at all in this post is the Dependency Injection Container. That's what is used in core to inject dependencies. Briefly, you provide configuration in the form of "service definitions" that tell the container what to actually inject when instantiating the class. What doesn't come through in the example above, because I was keeping it simple for the purpose of making the point about loose coupling, is that the SubPathAuto class would actually be a service itself, with a definition along the lines of

subpathauto:
  class: Drupal\subpathauto\SubPathAuto
  arguments: ['@path_processor_alias']

telling the container to inject the existing 'path_processor_alias' service when intantiating the SubPathAuto class.
And since all service definitions can be altered by any module, you could have another module alter this and specify something else to pass in.
Here are the symfony docs explaining how the DI container works http ://symfony.com/doc/current/book/service_container.html It is also covered in the DrupalCon Portland talk I linked to at the start of this post.
Posted on by Ryan Aslett.

Thanks, that's super helpful, Im really looking forward to working in D8.

Posted on by Mangalore (not verified).

You done a good job.. Thank you..

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

Filtered HTML

  • Use [acphone_sales], [acphone_sales_text], [acphone_support], [acphone_international], [acphone_devcloud], [acphone_extra1] and [acphone_extra2] as placeholders for Acquia phone numbers. Add class "acquia-phones-link" to wrapper element to make number a link.
  • To post pieces of code, surround them with <code>...</code> tags. For PHP code, you can use <?php ... ?>, which will also colour it based on syntax.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <h4> <h5> <h2> <img>
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.