Testing your controllers when you have a decoupled core

Posted on by Matthias Noback

A lot can happen in 9 years. Back then I was still advocating that you should unit-test your controllers and that setter injection is very helpful when replacing controller dependencies with test doubles. I've changed my mind: constructor injection is the right way for any service object, including controllers. And controllers shouldn't be unit tested, because:

  • Those unit tests tend to be a one-to-one copy of the controller code itself. There is no healthy distance between the test and the implementation.
  • Controllers need some form of integrated testing, because by zooming in on the class-level, you don't know if the controller will behave well when the application is actually used. Is the routing configuration correct? Can the framework resolve all of the controller's arguments? Will dependencies be injected properly? And so on.

The alternative I mentioned in 2012 is to write functional tests for your controller. But this is not preferable in the end. These tests are slow and fragile, because you end up invoking much more code than just the domain logic.

Ports and adapters

If you're using a decoupled approach, you can already test your domain logic using fast, stable, coarse-grained unit tests. So you particularly don't want to let your controller tests also invoke domain logic. You only want to verify that the controller correctly invokes your domain logic. We've seen one approach in Talk review: Thomas Pierrain at DDD Africa, where Thomas explained how he includes controller logic in his coarse-grained unit tests. He also mentioned that it's not "by the book", so here I'd like to take the time to explain what by the book would look like.

Hexagonal architecture prescribes that all application ports should be interfaces. That's because right-side ports should potentially have more than one adapter. The port, being an interface, allows you to define a contract for communicating with external services. On the left side, the ports should be interfaces too, because this allows you to replace the port with a mock when testing the left-side adapter.

Following the example from my previous post, this is a schema of the use case "purchasing an e-book":

PurchaseEbookController and PurchaseRepositoryUsingSql are adapter classes (which need supporting code from frameworks, libraries, etc.). On the left side, the adapter takes the request data and uses it to determine how to call the PurchaseEbookService, which represents the port. On the right side there is another port: the PurchaseRepository. One of its adapters is PurchaseRepositoryUsingSql. The following diagram shows how this setup allows you to invoke the left-side port from a test runner, without any problem, while you can replace the right-side port adapter with a fake repository:

Left-side adapter tests

Since the test case replaces the controller, the controller remains untested. Even though the controller should be only a few lines of code, there may be problems hiding there that will only be uncovered by exploratory (browser) testing or once the application has been deployed to production.

In this case it would help to create an adapter test for the controller. This is not a unit test, but an integrated test. We don't invoke the controller object directly, but travel the normal route from browser request to response, through the web server, the framework, and back. This ensures that we got everything right, and leave (almost) no room for mistakes in interpreting framework configuration (routing, security, dependency injection, etc.).

We have already established that you wouldn't want to test domain logic again through a web request. Two things we need to solve for adapter tests then:

  1. The controller shouldn't invoke the domain logic. Instead, we should only verify that it calls the port (which is an interface) in the right way. The pattern for this is called mocking: we need a test double that records the calls made to it and makes assertions about those calls (the number of times, and the arguments provided).
  2. We need a way to inject this mock into the controller as a constructor argument. This should only happen when the controller is invoked as part of an adapter test.

Three alternatives

There are three approaches that I can think of when you want to test a left-side adapter like a controller.

First, you could define an interface for each application service, like the PurchaseEbookService. This isn't a nice option in my opinion:

  1. You'd have an interface and a class that have the same name. So you'll resort to silly prefixes or suffixes like RealPurchaseEbookService or PurchaseEbookServiceInterface. The reason of course is that there is no real abstraction. You just need an interface so you can replace the injected dependency with a mock.
  2. You have an interface and a class for each application service. Which is certainly going to feel like a lot of extra files are needed. Decoupled application development already has a higher price in terms of number of files, so I wouldn't go for this.
final class PurchaseEbookService implements PurchaseEbookServiceInterface
{
    public function purchaseEbook(PurchaseEbook $command): PurchaseId
    {
        // ...
    }
}

interface PurchaseEbookServiceInterface
{
    public function purchaseEbook(PurchaseEbook $command): PurchaseId;
}

// in a test:
$expectedCommand = new PurchaseEbook(/* ... */);

$service = $this->createMock(PurchaseEbookServiceInterface::class);
$service->expects($this->once())
    ->method('purchaseEbook')
    ->with($expectedCommand)
    ->willReturn(new PurchaseId('...'));

Second, you could use a command bus. The interface is generic: a handle() method with a single argument for the command object. When you pass a PurchaseEbook object to it, it will forward the call to the PurchaseEbookService (or ...Handler):

interface CommandBus
{
    public function handle(object $command): mixed;
}

final class HardCodedCommandBus
{
    // ...

    public function handle(object $command): mixed
    {
        if ($command instanceof PurchaseEbook) {
            return $this->purchaseEbookHandler
                ->purchaseEbook($command);
        }

        // ...
    }
}

// in a test:
$expectedCommand = new PurchaseEbook(/* ... */);

$service = $this->createMock(CommandBus::class);
$service->expects($this->once())
    ->method('handle')
    ->with($expectedCommand)
    ->willReturn(new PurchaseId('...'));

In left-side adapters you can inject the command bus by its interface as a constructor argument.

The downsides of using the generic CommandBus interface are:

  • Lack of type inference: at the call site it's unclear what type of return value can be expected, because the handle() method's return type is mixed. Of course, you know it's returning a PurchaseId, but static analysis tools won't know, and neither will your IDE.
  • Obscure API: what commands does the CommandBus support? It's unclear. You'd have to look into its configuration and/or its code to find out.

Which is why I've been experimenting with a third approach: the application-as-an-interface. All the left-side ports are combined in a single interface, which you could call ApplicationInterface. You could also use the name of the module, subdomain, etc. like EbookStoreApplicationInterface.

interface ApplicationInterface
{
    public function purchaseEbook(object $command): PurchaseId;
}

final class Application implements ApplicationInterface
{
    // ...

    public function purchaseEbook(object $command): string
    {
        return $this->purchaseEbookService
                ->purchaseEbook($command)
                ->asString();
    }
}

// in a test:
$expectedCommand = new PurchaseEbook(/* ... */);

$application = $this->createMock(ApplicationInterface::class);
$application->expects($this->once())
    ->method('purchaseEbook')
    ->with($expectedCommand)
    ->willReturn('...');

This has the following advantages.

  1. Just like with the CommandBus, mocking gets easier: only a single interface has to be mocked, and you can choose ad hoc which methods require a mock or fake implementation.
  2. Unlike the CommandBus, the ApplicationInterface has a clear and explicit API: you know exactly what methods can be called from a left-side adapter like a controller.

Additionally, the ApplicationInterface wraps the entire Domain and Application layer (as explained in Does it belong in the application or domain layer?), which is a great of way of forcing domain-level implementation details behind an abstraction.

Using the application mock when testing

Now, how to use the ApplicationInterface mock when testing? You need to be able to rewire the dependency graph. That is, when the application runs in adapter-testing mode, the mock should be provided to e.g. the controller. Dependency injection containers usually have a feature for this, where you can override certain service definitions when the application runs in a certain environment. How you do that depends on whether you want to run the test against the web server or against the code. Frameworks like Symfony and Laravel provide options for both. If you run the test against the code, e.g. using Symfony's WebTestCase the test looks something like this:

$ebookId = '8e8aa978-f643-4036-9b85-70d6bed8d012';
$emailAddress = 'info@matthiasnoback.nl';

$expectedCommand = new PurchaseEbook($ebookId, $emailAddress);

$purchaseId = '48cd4ebf-b5f0-479f-82b8-dfd21e32266d';

$application = $this->createMock(ApplicationInterface::class);
$application->expects($this->once())
    ->method('purchaseEbook')
    ->with($expectedCommand)
    ->willReturn($purchaseId);

// Replace the real application with the mock
$this->getServiceContainer()->set(
    ApplicationInterface::class,
    $application
);

$this->client->request('GET', '/purchase-ebook');
$this->client->submitForm(
    'Purchase',
    [
        'purchase_ebook_form[ebookId]' => $ebookId,
        'purchase_ebook_form[emailAddress]' => $emailAddress
    ]
);

// We have been redirected to the purchase details page
self::assertEquals(
    '/purchase/' . $purchaseId,
    $this->client->getRequest()->getPathInfo()
);

This test verifies that, given a certain request (submitForm()):

  • The controller will call the purchaseEbook() method, providing the expected command object.
  • The controller will use the return value of this method to redirect to the purchase details page.

Since we have replaced the injected ApplicationInterface service, this doesn't actually invoke any domain logic. We only verify that the controller itself behaves well.

If you want to test against a real web server, you have to do a little bit more work. We'll discuss this some other time.

PHP testing hexagonal architecture controller
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Ben Roberts

Very interesting article - thanks.

Having this general ApplicationInterface is all well and good, and I thought this was interesting in your last post "Does it belong in the application or domain layer?" too.

However, surely this interface is going to grow quite a bit, as it will contain all of your use case commands for the module - and the implementation is going have all the dependencies as well. Seems like a candidate for a big ball of mud.

Would you only advocate this pattern if the target area of the application is focused? Or have multiple ApplicationInterface for each area of the app?

What other negatives have you discovered with this approach?

Biczó Dezső

Let me put on my framework extension (e.g.: bundles, plugins or modules) maintainer hat for a moment.

Would you use the ApplicantionInterface approach in framework extensions as a main entry point for provided features?  How could you provide BC promise for an interface like? For me, it seems there is a high chance that methods come and go in this interfaces, parameters can also change - which means regular major releases.  (Downstream developers could freak out ;P )

Having just a final Application class could be better in this situation because you can more easily add a BC layer (nullable parameters, alternative methods, etc) for a time being and you can communicate upcoming changes with @trigger_error('XY is deprecated in my_component:1.10.2 and will be removed in my_component:2.0.0', E_USER_DEPRECATED). Although, if you have a final class only, mocking becomes a little bit complicated, but not impossible.  Plus if every dependency of an Application has an interface (which may or may not be part of a extension's public API), you can easily swap them. This approach could also work with other, more granular services, which makes the service collector solution unnecessary.

PS: I must admit you perfectly summarized my struggling with having interfaces for almost every service is Hexa/Ports&Adapters which makes complicated to limit the public API that I would like to expose for downstream developers. There is also an other pattern that I have used and saw others are using it too: MyModule/Foo/Contracts/MyService (which is the interface) and MyModule/Foo/MyService which is a default implementation that implements the interface by using aliasing (e.g: final class MyService implements MyServiceInterface)

Matthias Noback

Great question, Biczó. I honestly don't think the ApplicationInterface approach will be very useful for frameworks. The purpose is being able to replace the complete domain logic with a single test double. Frameworks, I think, are better off with single-purpose contracts (e.g. a Router interface).

Something else to consider: how can a framework can better support the ApplicationInterface approach, or a ports/adapters approach in general?

Matthias Noback

Hi Ben,

I totally agree that these are valid concerns ("Does it scale?" :). Yes, I think this works best with smaller, focused modules. I haven't done this in practice, but it may even be a great way to keep contexts more "bounded", that is, let contexts communicate between each other via ContextInterfaces. I'm still looking for more design feedback on this.

To keep the number of dependencies down, you may also delegate the call to a service locator, like most command buses do anyway. E.g. $this->container->purchaseEbookService()->purchaseEbook().