Wednesday 16 September 2015

Write Unit Tests for Drupal 7 Code

In this article I show you how this can be applied to a real life example.

TL;DR

I encourage you to start testing your code. Here are the most important points of the article:

Dependency Injection and Service Container

Jeremy Miller defines dependency injection as:
[...] In a nutshell, dependency injection just means that a given class or system is no longer responsible for instantiating their own dependencies.
In our MyClass we avoided instantiating CacheController by passing it through the constructor. This is a basic form of dependency injection. Acoording to Martin Fowler:
There are three main styles of dependency injection. The names I'm using for them are Constructor Injection, Setter Injection, and Interface Injection.
As long as you are injecting your dependencies, you will be able to swap those objects out with  their test doubles in your unit tests.
An effective way to pass objects into other objects is by using dependency injection via a service container. The service container will be in charge of giving the receiving class all the needed objects. Then, the receiving object will only need to get the service container. In our System Under Test (SUT), the service container will yield the actual objects, while in the unit test domain it will deliver mocked objects. Using a service container can be a little bit confusing at first, or even daunting, but it makes your API more stable and robust.
Using the service container, our example is changed to:

class MyClass implements MyClassInterface {
  // ...
  public function __construct(ContainerInterface $service_container) {
    $this->cacheController = $service_container->get('cache_controller');
    $this->anotherService = $service_container->get('my_services.another_one');
  }
  // ...
  public function myMethod() {
    $cache = $this->cacheController->cacheGet('cache_key');
    // Here starts the logic we want to test.
    // ...
  }
  // ...
}
Note that if you need to use a new service called 'my_services.another_one', the constructor signature remains unchanged. The services need to be declared separately in the service providers.
Dependency injection and service encapsulation is not only useful for mocking purposes, but also to help you to encapsulate your components –and services–. Borrowing, again, Jeremy Miller’s words:
Making sure that any new code that depends on undesirable legacy code uses Dependency Injection leaves an easier migration path to eliminate the legacy code later with all new code.
If you encapsulate your legacy dependencies you can ultimately write a new version and swap them out. Just like you do for your tests, but with the new implementation.
Just like with almost everything, there are several modules that will help you with these tasks:
  • Registry autoload will help you to structure your object oriented code by giving you autoloading if you follow the PSR-0 or PSR-4 standards.
  • Service container will provide you with a service container, with the added benefit that is very similar to the one that Drupal 8 will ship with.
  • XAutoload will give you both autoloading and a dependency injection container.
With these strategies, you will write code that can have it’s dependencies mocked. In the previous article I showed how to use fake classes or dummies for that. Now I want to show you how you can simplify that by using Mockery.

Mock your objects

Mockery is geared towards providing even more flexibility when creating mocks. Mockery is not tied to any test framework which makes it useful even if you decided to move away from PHPUnit.
In our previous example the test case would be:
// Called from the test case.
$fake_cache_response = (object) array('data' => 1234);
$cache_controller_fake = \Mockery::mock('CacheControllerInterface');
$cache_controller_fake->shouldReceive('cacheGet')->andReturn($fake_cache_response);
$object = new MyClass($cache_controller_fake);
$object->myMethod();
Here, I did not need to write a CacheControllerFake only for our test, I used Mockery instead.
PHPUnit comes with a great mock builder as well. Check its documentation to explore the possibilities. Sometimes you will want to use one or the other depending on how you want to mock your dependency, and the tools both frameworks offer. See the same example using PHPUnit instead of Mockery:

// Called from the test case.
$fake_cache_response = (object) array('data' => 1234);
$cache_controller_fake = $this
  ->getMockBuilder('CacheControllerInterface')
  ->getMock();
$cache_controller_fake->method('cacheGet')->willReturn($fake_cache_response);
$object = new MyClass($cache_controller_fake);
$object->myMethod();

No comments:

Post a Comment