Skip to main content

Writing Testable Drupal 8 Services

Back-end Development
Drupal

Most of us are aware of the general principals of writing testable code.  Drupal 8, based on Symfony, leverages a service based architecture where the services themselves are modular and the injected dependencies for each service class can be derived from a number of places.  On a Drupal site, in normal operation these dependencies will come from the "Container", which uses the definition in your mymodule.services.yml.

When testing, you can still leverage the container to pull services from, but it is also necessary at times to pass mock dependencies to your services in order to provide predictable results.

See the post on setting up a kernel test for an overview of writing tests in Drupal 8. This article will continue to use the example of a service that uses networking resources.

The relevant parts of our example are to:

  • Authorize a user via an external authentication system
  • Reconcile this information with Drupal, creating or mapping a Drupal user to the one on the remote system.

We could easily write a service that handles both of these things.  A method might leverage guzzle to make the networking call to a REST endpoint, process the results, handle failures and exceptions, and also talking with the Drupal framework to make sure a Drupal user is configured appropriately that represents the external user.  But to make this more testable it would be good to separate the network activity from the business logic that interacts with Drupal. 

What would be so hard with a single service?

The most important thing to consider when building testable services is that you will need to provide all the dependencies for that service.  When writing a test that makes external calls to another system, how will you inform your test that it should not actually make calls to these external systems and instead provide a number of predictable scenarios that can tests can be written for?

We could pass in arguments to our service that indicate it is a test, and should react differently when making networking calls.  But this will not test all of the actual code that will be used when run on your site.  So it may provide some helpful testing, but going forward this awkward relationship the service has with real world data and test data will need to be maintained.  In this scenario I can say from personal experience that eventually the test code will become obsolete and only be a source of clutter in your class.

Abstracting network activity

As I mentioned in the beginning of the article, it would be better to separate the network stuff from the business logic.  But why?

What if your business logic didn't have any mention or concern about where the network requests were going, or even that the data was coming from an external system at all?  By pulling the network requests out of the equation we can test just the business logic, just as though it were running on your live site.  The network service that interacts with the external system can just be another dependency for the user authentication service.  When writing our test we can create a network service class that "mocks" the network calls and pass that to our authentication service. 

Check out this article for how to mock network requests.

Here is a simple diagram to illustrate what I am suggesting.  It's not the easiest thing (for me) to diagram, so I'll explain.  The normal site will use this service which will inject the normal networking service via the container.  The kernel test will fake out the authentication service and provide an implementation of the network service that returns predefined responses.

Network Test Diagram

Your mock network class can be something as simple as hard-coding the results in a legitimate class that implements the same networking interface as the live code.  However, using mocks described in this article simplifies things greatly, and we have no need to create yet another file/class just for testing.

Hopefully this illuminates some of the benefits to separating logic in order to make testing easier.