Qafoo GmbH - passion for software quality
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:Author: Benjamin Eberlei
:Date: Fri, 02 Sep 2016 07:59:09 +0200
:Revision: 8
:Copyright: All rights reserved
Outside-In Testing and the Adapter and Facade Patterns
======================================================
:Keywords:
php, testing, design pattern, adapter, facade
:Description:
Outside-In Testing benefits a lot from the facade and adapter patterns,
which will separate your code from third-party libraries and simplify
the APIs of your system.
:Abstract:
Outside-In Testing benefits a lot from the facade and adapter patterns,
which will separate your code from third-party libraries and simplify
the APIs of your system.
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.
.. warning::
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::
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::
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::
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::
createSampleProduct();
\Phake::when($client)->get($url)->thenReturn('');
\Phake::when($parser)->parse('')->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.
::
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::
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.
.. note::
Qafoo can help you finding your right testing strategies – read about
possible workshop here__. We can, of course, adapt those schedules to your
needs.
__ /services/workshops/testing.html
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.
..
Local Variables:
mode: rst
fill-column: 79
End:
vim: et syn=rst tw=79
Trackbacks
==========
Comments
========
- Felix at Tue, 05 Jul 2016 13:45:29 +0200
Nice article, thanks!
There is one Typo "RemoveCatalog" on the implementation part.
- Toby at Tue, 05 Jul 2016 14:05:48 +0200
@Felix: Thanks. I fixed the typo.
- Thomas at Wed, 06 Jul 2016 11:30:05 +0200
I'm missing one aspect: Will you (partly) replace the mock objects after
implementing the real objects (if there are no reasons not to do that, e.g.
execution time, external resources, ...). Alternatively: Do you write
additional integration tests in the end?
My main issue with mock objects is that the mocked methods can differ from
the real implementation, e.g. return types.
I know that there are issues with using the real implementation of the
depended-on components (DOC) instead of mocks as well - e.g. finding out if
some failure is in the system under test or in a DOC. But that hasn't been a
real issue for me in practice.
Regards Thomas
- Nagarjun at Tue, 16 Aug 2016 20:09:30 +0200
Hi
Our client is looking for PHP Developer(75% remote). If you are interested
please send me your updated resume along with visa status and
current/expecting salary.
Role: PHP Developer (75% remote) Location: Hoover, AL Possibility of remote
with 1-2 days onsite weekly
Requirement: Candidate must have experience with web development (PHP, CSS,
JQuery, and AJAX) and programming in codeignitor environment is a plus.
Proven experience programming in PHP, CSS, JavaScript, JQuery and AJAX
Proven knowledge of SQL environment and SQL statements PHP, CSS, MySQI,
JQuery, and AJAX programming in codeignitor environment is a plus. At least
4 years experience is desirable.
Thanks & Regards:- Name: Nagarjun Bandaru Email id: nagarjun@maania.com