Outside-In Testing and the Adapter and Facade Patterns

First published at Tuesday, 5 July 2016

This blog post has first been published in the Qafoo blog and is duplicated here since I wrote it or participated in writing it.

Warning: This blog post is more then 8 years old – read and use with care.

Outside-In Testing and the Adapter and Facade Patterns

We at Qafoo are big fans of outside-in testing as described in the book "Growing Object-Oriented Software, Guided by Tests" (Steve Freeman and Nat Pryce). As part of our workshops on Test-Driven Development we explain to our customers how testing from the outside-in can help find the right test-mix.

The technique puts a focus on test-driven-development, but instead of the traditional approach starts at the acceptance test level. The first test for a feature is an acceptance test and only then the feature is implemented from the outside classes first (UI and controllers), towards the inner classes (model, infrastructure). Mocks are used to describe roles of collaborators when starting to write tests for the outside classes. When a class is tested, the roles described by interfaces are implemented and the testing cycle starts again with mocks for the collaborators.

Outside-In testing leads to interfaces that are written from what is useful for the client object using them, in contrast to objects that are composed of collaborators that already exist. Because at some point we have to interact with objects that exist already, we will need three techniques to link those newly created interfaces/roles to existing code in our project:

  1. The adapter pattern, mostly for third-party code

  2. The facade pattern, mostly to structure your own code into layers

  3. Continuous refactoring of all the interfaces and implementations

This blog post will focus on the facade adapter pattern as one of the most important ingredient to testable code.

Even if very well tested, APIs of third party libraries or frameworks are usually not suited for projects using any form of automated testing, either because using them directly requires setting up external resources (I/O) or mocking them is complex, maybe even impossible.

Side Fact: This even affects libraries that put a focus on testing, such as Doctrine2, Symfony2 or Zend Framework. The reason is that libraries often provide static APIs, fluent APIs, facades with too many public methods or complex object interactions with law-of-demeter violations.

Take a common use-case, importing data from a remote source into your own database. In a Symfony2 project with Doctrine and Guzzle as supporting libraries, the feature could easily be implemented as a console command, using only the third-party code and some glue code of our own:

<?php namespace Acme\ProductBundle\Command; class ImportProductCommand extends ContainerAwareCommand { protected function configure() { /* omitted */ } protected function execute(InputInterface $input, OutputInterface $output) { $client = $this->getContainer()->get('guzzle.http_client'); $entityManager = $this->getContainer()->get('doctrine.orm.default_entity_manager'); $request = $client->get('http://remote.source/products.xml'); $response = $request->send(); $products = $this->parseResponse($response); foreach ($products as $product) { $entityManager->persist($product); } $entityManager->flush(); } protected function parseResponse($response) { /** omitted */ } }

Looks simple, but in the real world, you can safely assume there is quite some complexity in parseResponse and possibly even more logic inside the loop over all the $products.

In this scenario, the code is completely untestable, just by combining the APIs of Guzzle, Doctrine and Symfony.

Lets take a different approach, starting with the highest level description of the feature as a test written in Gherkin for Behat:

Scenario: Import Products Given a remote service with products: | name | description | price | A | Nice and shiny | 100 | B | Rusty, but cheap | 10 When I import the products Then I should see product "A" in my product listing Then I should see product "B" in my product listing

This will be our acceptance (or end-to-end) test that will use as many external systems and I/O as possible. We might need to mock data from the remote product service, but when possible we should use the real data. This test will require the UI to be done as well as the necessary database and remote system APIs and therefore will fail until we have implemented all the code necessary. So lets focus on the code directly with our first unit-test and lets imagine how we want the code to be used:

<?php namespace Catalog; class ImportProductTest extends \PHPUnit_Framework_TestCase { public function testImportTwoProducts() { $remoteCatalog = \Phake::mock('Catalog\RemoteCatalog'); $productGateway = \Phake::mock('Catalog\ProductGateway'); $productA = $this->createSampleProduct(); $productB = $this->createSampleProduct(); \Phake::when($remoteCatalog)->fetch()->thenReturn(array($productA, $productB)); $importer = new ProductImporter($productGateway); $importer->import($remoteCatalog); \Phake::verify($productGateway)->store($productA); \Phake::verify($productGateway)->store($productB); } }

When we fetch 2 products from the $remoteCatalog then those products should be passed to the $productGateway, our storage system. To describe the use-case in this simple way, we have abstracted RemoteCatalog and ProductGateway, which will act as facades used in our ProductImporter. The implementation is very simple:

<?php namespace Catalog; class ProductImporter { /** * @var ProductGateway */ private $productGateway; public function import(RemoteCatalog $remoteCatalog) { $products = $remoteCatalog->fetch(); foreach ($products as $product) { $this->productGateway->store($product); } } }

We haven't seen the Guzzle and Doctrine code here again, instead we have written code that is entirely written in our business domain and uses concepts from this domain.

Lets move to the implementation of the RemoteCatalog starting with a test:

<?php namespace Catalog\RemoteCatalog; class HttpCatalogTest extends \PHPUnit_Framework_TestCase { public function testGetAndParseData() { $client = \Phake::mock('Catalog\Adapter\HttpClient'); $parser = \Phake::mock('Catalog\RemoteCatalog\Parser'); $url = 'http://remote.local'; $productA = $this->createSampleProduct(); \Phake::when($client)->get($url)->thenReturn('<xml>'); \Phake::when($parser)->parse('<xml>')->thenReturn(array($productA)); $catalog = new HttpCatalog($url, $client, $parser); $data = $catalog->fetch(); $this->assertSame(array($productA), $data); } }

This test again perfectly clear explains how fetching a catalog with HTTP should work on a technical level. Notice how we choose a very simple API for the HTTP client, one that is fetching and $url and retrieving the body as string. We don't need more.

<?php namespace Catalog\RemoteCatalog; use Catalog\RemoteCatalog; class HttpCatalog implements RemoteCatalog { private $url; private $client; private $parser; public function fetch() { $body = $this->client->fetch($this->url); return $this->parser->parse($body); } }

The HttpClient is a real adapter for us now, we want this very simple API and we are going to use Guzzle to implement it:

<?php namespace Catalog\Adapter\Guzzle; use Guzzle\Http\Client; class GuzzleHttpClient implements HttpClient { private $client; public function __construct(Client $client) { $this->client = $client; } public function fetch($url) { $request = $this->client->get($url); $response = $request->send(); return $response->getBody(true); } }

Implementing the Guzzle client we have reached a "leaf" of our object graph. It is important to see how only the Guzzle adapter actually uses Guzzle code. A complete solution would also require to handle the Guzzle Exceptions, but that is only a trivial task to introduce as well.

You can continue with this example and implement the Parser and the ProductGateway objects. The technique stays the same: Think in terms of what you want the API to look from the outside and invent collaborators that help you think about the problem. Then implement them until you get to the "leafs" of the object graph. Only the leafs should actually contain code to third party software.

Starting with new requirements of the system we will probably be able to reuse some of the code: The HttpClient interface is very useful in a good number of use-cases. The HttpCatalog can be used with specific Parser implementations to import many different product catalogs that have an HTTP interface. And if your customer uses some other protocols like FTP, BitTorrent or anything else then we are good as well. The ProductGateway will probably be used in various places to give us access to finding and storing products.

Conclusion

Specification and testing of the requirements from the outside requires you to think about what you need first and not on what you have and how you might combine this. This helps to design simple and reusable objects.

But outside-in testing has additional benefits: When you don't test everything in your system with unit-tests (which is not uncommon) for any of the various reasons (time, prototyping, not core domain, ..) then you still have at least one acceptance test that verifies the complete feature and maybe some unit-tests for the tricky implementation details.

Overall we think making yourself familiar with outside-in testing is beneficial to designing testable and maintainable applications.

Subscribe to updates

There are multiple ways to stay updated with new posts on my blog: