Sunday, 29 March 2020

Hexagonal architecture: don't get lost on your right-side

Fan of hexagonal architecture and its promises for a very long time, I often spent time to understand it, explain it but also to demystify its implementation. On the other hand, I've often limited my explanations to the simple cases. What we call the “happy path” where everything goes well. In this first article of an upcoming miniseries, I want to talk about some implementation details as well as traps in which it is very easy to fall. Finally, I will take some time to talk about how to test your hexagon, suggesting a non-orthodox strategy. The idea being to save you a little time during your implementations of this incredibly useful architectural pattern.

The "non-cohesive right-side adapter" anti-pattern

We will be starting today with a design-smell: the non-cohesive right-side adapter. If you do not know hexagonal architecture yet (a.k.a.: ports and adapters), I suggest you start by reading my previous post on the subject which summarizes all this. Because to explain the non-cohesive right adapter anti-pattern, we are just going to recall here a few fundamentals about Alistair COCKBURN’s hexagonal architecture:
  • We split our software in 2 distinct regions: 
    • The inside (for the Domain / business code that we call "the Hexagon")
    • The outside (for the Infrastructure / tech code)
  • To enter the Hexagon and interact with the domain, we talk to a port that we call "driver port" or "left-side port"
  • The domain code will then use one or more third-party systems (data stores, web APIs, etc) to gather information or trigger side-effects. To do so, the business code must remain at its business-level semantic. It is done by using one or more "driven ports" or "right-side ports".
  • A port is therefore an interface belonging to the Domain using business semantics to express either requests that we address to our system (left-side port) or external interactions that it performs along the way to achieve its goal (right-side port). I like to see ports as drawbridges we are using to come and go between the infrastructure side and the Domain one (analogy that Cyrille MARTRAIRE had whispered to me a very long time ago)
  • In terms of dependency, the infrastructure code knows and references the Domain one. But let's be more precise here (BTW, thank you for your feedback Alistair ;-): only the left-side infrastructure code knows the Domain actually (to interact with it through a left-side port).
  • On the other hand, the Domain (business) code must never reference any technical/infrastructure-level code. It works at runtime thanks to the Dependency Inversion principle, that Gerard MESZAROS called once "configurable dependencies" (which lends itself very well to Alistair's pattern)
  • An adapter is a piece of code which makes it possible to pass from one world to another (infra => domain or domain => infra) and which takes care in particular of converting the data structures of a world ( ex: JSON or DTO) to the other (ex: POJO or POCO of the domain)
  • There are 2 kind of adapters:
    • "Left-side adapters" which use a left-side port to interact with the Domain (thus having an aggregation-type relationship with it)
    • "Right-side adapters" which implement a right-side port instance

The problem with the right wing...

You may have noticed here that I did not say that a right-side adapter implements multiple right-side ports. It was intentional.
Because if you did, there is a good chance that you will land on the anti-pattern that I want to mention here: the non-cohesive right adapter.
This is what happens when you give too much responsibility to a right-side adapter: you risk then deporting part of the Domain logic of your hexagon into one of its peripheral elements on the infrastructure side (i.e. the adapter). This is what happens especially when an adapter references and uses another adapter directly.
Of course, this is something I strongly advise against doing. Indeed, you need to keep all the business orchestration logic at the Domain level and not to distribute it here and there at the infrastructure code level. But since I've seen that everyone at least falls into this trap once, it's worth mentioning. Avoid this situation if you don't want to end up with an anemic Domain.
(click on the image to zoom)
 (click on the image to zoom)

Worst. In that case, those who test only the center of the hexagon (i.e. the domain code) by stubbing the adapters all around would miss out on lots of code and potential bugs. Those would be tucked away in your untested adapters.
It is indeed extremely easy to deceive yourself by putting a lot of implicit in the behaviours of your adapter stubs. You will very often put in those stubs what you think the adapters will do or should do. But the risk afterwards is to fail to implement the concrete adapter properly (when you will forget all the implicit you initially put in your stubs in the first place). This will give you a false confidence with a fully green test harness, but potentially buggy behaviours in production (when using the real concrete right-side adapters).

Let's talk a little bit about test strategy

I have already written and spoken on the issue but since I have not yet translated into English my most specific article on the subject, I will just clarify here some essential terms to understand the rest of the discussion.
After more than 15 years of practicing TDD at work, I have arrived over the years to a thrifty form of outside-in TDD which suits 90% of my projects and contexts very well. Outside-in means that I consider my system (often a web API) as a whole and from the outside. A bit like a big black box that I’m going to grow and improve gradually by writing acceptance tests that will drive its behaviours and reliefs (see the fantastic GOOS book from Nat Pryce and Steve Freeman if you want to know more). Those acceptance tests aren’t interested in implementation details (i.e. how the inside of the black-box is coded) but just on how to interact with the box in order to have the expected reactions and behaviours. This lets me easily change the internal implementation of my black box whenever I want without slowing me down or breaking fragile tests.
My acceptance tests are therefore coarse grained tests covering the full box (here, the entire hexagon) but not using real technical protocols neither real data stores in order to be the fastest possible (I'm addicted to short feedbacks and thus use a tool to run my tests every time I change a line of code).
Besides these acceptance tests which constitute 80% of my strategy, I generally write some fine-grained unit tests (the small tests of the "double loop" when I need them), some integration tests, and some contract tests for my stubs to check that they behaves like the real external dependencies they substitute with (and also to detect when something change externally). The rest of this article will mainly talk about my coarse-grained acceptance tests.

The best possible trade-off

This parenthesis about testing terminology and my default test strategy being closed, let's focus on our initial problem: how to avoid writing incomplete tests allowing bugs to happen at runtime when we deploy our system on our server.
My recommendation: write acceptance tests that cover your entire hexagon, except for the I/O at the border. Yes, you heard me right: I advise you to write acceptance tests that use all your concrete adapters and not stubs for them. The only thing you will have to stub are network calls, disk or database access which are made by your adapters. By doing so, you test all your code in full assembly mode (without illusions or unpleasant surprises in production therefore), but blazing fast ;-)

 (click on the image to zoom)
(click on the image to zoom)

 

Can you give me an Example?

Sure. Let's take an Example to make the implicit, explicit.
Imagine that you have to code a sitemap.xml file generator for the SEO of a web platform that has a completely dynamic content controlled by several business lines (e-biz, marketing). This hexagonal micro-service could be a web API called daily in order to regenerate all sitemaps of the related website. You call a HTTP POST request, and all the sitemap files are then updated and published. (as a side-note: Please, don't tell my mum that I mentioned microservices in one of my articles ;-)
Okay. Well instead of writing acceptance tests like this where every adapter is stubbed:
  (click on the image to zoom the code)

I rather advise you to write acceptance tests in which you will include your concrete adapters and only stub their last-mile I/O. It is worth mentioning here that I’m using my ASP.NET web controller as my left-side adapter. Anyway, it looks like this: 
(click on the image to zoom the code)

There is a better way

Of course, in order to have readable, concise (no more than 10/15 lines long) and expressive acceptance tests (i.e. by seeing clearly the values ​​transmitted to your stubs), you will generally go through builders for your stubs, mocks. You can also use them to build your hexagon (which is a 3 steps initialization process). I didn't show it earlier because it would have been less clear for my example but a target version might look like this:
 (click on the image to zoom the code)

A good adapter is a pretty dumb adapter

Be careful. Testing your full hexagon with its concrete adapters should not prevent you from putting all your business logic or orchestration at the heart of your Domain code (i.e. the heart of the hexagon).
A good adapter is a pretty dumb adapter. As a reminder, its role is to translate a model linked to infrastructure to a business model and vice versa + to make the link with the necessary technology. Above all, you should not have a situation where an adapter uses another adapter to play tricks or optimize certain calls.
When you are doing Domain Driven Design, you aim to reduce as much as possible the accidental complexity of your system (i.e. the complexity related to your tools, idioms or programming language) while trying to focus only on essential complexity (i.e. the complexity related to the business problem to be solved). In that context, the infrastructure code of your hexagon should be a pass-through. No more, no less (note: we won’t talk about Anti-corruption Layers here. That will be worth another article).

Expected objections

People generally agree on the need to keep the domain in the center of the Hexagon. On the other hand, I've got some objections related to my testing strategy. Let's review some of them.


First, it could be objected here that my acceptance tests are a bit hybrid and that they cover both Domain considerations but also technical ones. Not very orthodox...
Well... actually I don't realy care about orthodoxy or by-the-book-ism ;-) As soon as you can explain your choices and the trade-offs you, I'm ok with that. Moreover it can be completely transparent if you make sure that each of your acceptance tests covers business behaviours of your system (service / API / microservice...). It's up to you to work on the expressiveness of your tests: their names, their simplicity, their ubiquitous language, but also the conciseness of the "arrange" steps (using DSL-like expressivity for example).
I've landed on this new strategy this year after tons of trials and errors. Before that I was implementing the very same test strategy often put forward by the London friends from Cucumber (Seb Rose, Steve Tooke and Aslak Hellesøy). That one aims to combine:
  • A lot of acceptance tests but for which we stub all the adapters (thus blazing fast tests)
  • And some additional integration/contract tests aside, in another project. These contract tests verify that our adapter stubs used in our acceptance tests have exactly the same behaviour as their real concrete adapters (the one that we package and deliver with our hexagon in production). These last integration tests are therefore much slower and run more rarely (mainly on the dev factory, not within my local NCrunch automatic runner). But no surprise here: cause it's an expected trade-off

So, we have had a really interesting situation here, which allowed us to have tons of super-fast acceptance tests AND to be confident enough about our right-side adapter stubs to be true to reality enough (for some reference scenarios).
I talk about it in the past-tense here because I haven't really managed to be confident enough with this setup during my various experiences with different teams.

Blind spot

Indeed, I regularly had problems with this strategy because we didn't cover enough cases or errors or exceptions within those contract tests of our right-side adapters. Too many happy paths (basic scenarios where everything goes well), not enough corner cases and exceptions in those contract tests. To put it another way, our acceptance tests were asking our stubs on many more cases than what was planned in their contract tests.
We developers are attracted to the happy path as much as moths are to a lit bulb. It seems to be one of our determinism (quite the opposite of QA people ;-) I knew that already. But I observed these situations carefully to try to understand what had made us fail here (in my various teams and contexts). I came to the conclusion that it was because these integration tests - in addition to being very "plumbing oriented" (more legacy-oriented than domain-oriented) - were much slower to run than our unit or acceptance ones. Every time you add a new parameter combination, it increases the overall test harness execution latency. That’s why my people paid less attention to it. A little bit in mode: "anyway, it's an integration test for a stub... it should do the job but we're not going to spend too much time and effort on it either".
As a consequence, we used to test a lot less case combinations within these contract tests for our stubs than what we used to do in our other tests (i.e. coarse-grained acceptance tests or fine-grained unit tests).

Stubbing less

The side-effect of this was that combination of contract tests and acceptance tests was not sufficient, to manage to catch all the bugs or plumbing problems in our final assembly. It is therefore for these reasons that I finally arrived at the test strategy that I presented to you in this article, and which includes the concrete adapters in our acceptance tests. On the other hand, I continue to use this strategy of testing contracts for external components or third-party APIs. But now I stub less things, my stubs only cover a very fine part of my system.

One can Pick hexagonal architecture for different reasons

This one is important. As a final warning, I should also point out that my testing strategy has been designed and worked well in my contexts so far. Of course, I’m not saying here that this is a one size fits all situation (I don’t even think that such a situation exists).
In particular, my contexts aren’t the one faced by Alistair when he created the pattern. Back in the days, Alistair had to find a solution in order to survive with a huge number of connectivity and technologies, to avoid his Domain logic to suffer from a combinatorial explosion (for a weather forecast system).
At work, I mainly use hexagonal architecture pattern because it allows me to split my business code from my technical one. Not to combine or easily switch my left-side technologies and adapters. Reason why I usually have a unique exposure for my business services which fits exactly my purpose and context: REST-like HTTP for a web API, Graph QL sometimes for website backends, Aeron middleware for a low latency service, AMQP-based middleware for some HA, resilient and scalable services, RPC for... (no, I'm kidding, I hate so much the RPC paradigm ;-)
More than that, it is very likely that the communication middleware I picked has more impacts on my business code interactions (left-side port) than a simple switch of Adapters. In some cases, the middleware technology I use may even impact my programming paradigm (event-driven reactive programming/Classical OOP/FP/lame OOP/transaction script). Reason why my left-side port may often become a leaky abstraction.
As a consequence, I only need one adapter on the left side. This saves me from having to run all my acceptance tests as many times as I would have different left-side exposure technologies. That is worth mentioning (at least for people that would have a similar objective that Alistair had).
And when my goal is to build a REST-like web APIs, I even use my web controllers as left-side adapters instead of creating another ad-hoc adapter type (this is something we are legion to do, if I recall Everyone who chatted with me during conferences or my DDD training sessions).

An interesting side-effect

The technique I was promoting here to test everything but not the I/O will put you in a very comfortable situation at the end of the day. Indeed:
  • your acceptance tests will cover a base code very faithful to the reality of the production (my main driver here)
  • your acceptance test harness will allow you to calmly refactor your code base in the event that you have made a mistake and put some business behaviour into your adapters. Thanks to this test harness covering everything, the move of the wrongly located business code from the right-side Adapter to the Domain can be done without any risk.

Perhaps we could find an easy way (other than pairing or code review) to prevent less experienced people from falling into this trap. For the moment, this ability to easily refactor -a posteriori- when we screwed this (or when we took a little short-term technical debt), was more than enough for my projects.

Conclusion

Since this post is long enough, I’ll be brief:
  1. Always go through the center of the hexagon, do not connect your right-side adapters to each other
  2. Do not code domain logic or domain orchestration logic in your adapters
  3. Test your entire hexagon (including adapters). To do so, only stub your last miles I/O from your right-side adapters.
  4. Aside, keep testing your test doubles or stubs of external dependencies against real implementations via contract-based integration tests.

The next 2 articles in this series dedicated to hexagonal architecture will talk about the subject of health checks (how to know if our hexagon is in shape or not) but also about the comparison with an alternative to the pattern: Functional Core (with imperative shell). 
Happy Coding! See you soon.
Thomas

Wednesday, 25 March 2020

Architecture Hexagonale : ne vous perdez pas à droite


Fan de l’architecture hexagonale et de ses promesses depuis très longtemps, j’ai souvent passé du temps à la comprendre, l’expliquer mais aussi à démystifier son implémentation. Ce pattern a d’ailleurs accompagné une grosse partie de mes premières mises en œuvre du DDD au travail. Mais comme beaucoup trop de développeuses et de développeurs, je me suis souvent arrêté aux cas simples dans mes explications, aux “happy path” où tout se passe bien. Dans ce premier article d’une mini-série à venir, je souhaite évoquer cette fois quelques détails d’implémentation ainsi qu’un piège dans lequel il est très facile de tomber. L’idée étant de vous faire gagner un peu de temps lors de vos mises en œuvre de ce pattern d’architecture toujours aussi incroyablement utile.


Un Design-smell pour votre hexagone

Une fois n’est pas coutume, on va parler aujourd’hui d’un design-smell ou d’un anti-pattern : l’adaptateur de droite non cohésif. Si vous ne connaissez pas encore l’architecture hexagonale, je vous suggère de commencer par la lecture de mon précédent post sur le sujet qui récapitule tout ça ( http://tpierrain.blogspot.com/2016/04/hexagonal-layers.html). Car pour expliquer l’anti-pattern adaptateur de droite non cohésif, on va juste rappeler ici quelques fondamentaux au sujet de l’architecture hexagonale :

  • Pour interroger le métier (le code du domaine), on s’adresse à un port que l’on appelle « driver port » ou « left-side port »
  • Pour que le code du domaine puisse faire son travail, celui-ci peut avoir besoin d’utiliser des systèmes tierces (data store, web API, etc.). Pour ce faire, le code métier reste au niveau d’expressivité métier en interrogeant un ou plusieurs « driven ports » ou « right-side ports ».
  • Un port est donc une interface (qui fait partie du Domaine) qui utilise une sémantique métier pour exprimer des demandes que l’on adresse à notre système (left-side) ou des actions/demandes que celui-ci effectue en cours de route pour arriver à ses fins (right-side). J’aime bien voir les ports comme des pont-levis pour aller et venir entre l’infra et le domaine et vice-versa (analogie que m’avait soufflée Cyrille MARTRAIRE il y a très longtemps)
  • En termes de dépendance, la technique (le code d’infrastructure) connait et référence le métier. Enfin, soyons précis (au passage, merci à Alistair COCKBURN pour cette remarque pertinente) : seul le code d’infrastructure qui se trouve à gauche de l’hexagone connait le métier (à travers les portes d’entrée vers celui-ci que constituent les ports de gauche), pour interagir avec celui-ci.
  • A l’inverse, le code métier (domaine) ne référence pas et ne connait pas le niveau technique. L’ensemble arrive à fonctionner au runtime grâce à l’inversion de dépendance que Gerard MESZAROS appelle lui des « configurable dependency » (ce qui se prête très bien au pattern d’Alistair)
  • Un adapteur est un bout de code qui permet de passer d’un monde à l’autre au runtime (infra => domain ou domain => infra) et qui s’occupe notamment de convertir les structures de données d’un monde (ex : JSON ou DTO) à l’autre (ex : POJO ou POCO du domaine)
  • Il y a 2 types d’adaptateurs : 
    • Les « left-side adapters », qui ont une relation de type agrégation avec une instance de left-side port qu’ils utilisent
    • Les « right-side adapters », qui implémentent une instance de right-side port


J’ai connu un adaptateur de droite une fois, qui avait dix fois plus de classes...

Vous aurez peut-être remarqué ici que je n’ai pas dit que les right-side adapters implémentaient plusieurs interfaces de right-side ports. C’est intentionnel.

Car si vous le faisiez, il y a de fortes chances pour que vous arriviez à l’anti-pattern que je veux évoquer ici : l’adaptateur de droite non cohésif.

De quoi s’agit-il ? C’est ce qui se passe lorsque vous confiez trop de responsabilités à un adaptateur de droite : vous risquez alors de déporter une partie de la logique de l’hexagone dans un de ses éléments périphérique (l’adaptateur). C’est ce qui se passe notamment lorsqu’un adapteur référence et utilise en direct un autre adapteur. C’est bien entendu quelque chose que je déconseille vivement (pour conserver toute la logique d’orchestration métier au niveau du domaine et pas la repartir au niveau du code d’infrastructure), mais vu que tout le monde tombe dans ce piège au moins une fois, ça vaut le coup d’être mentionné. Fuyez cette situation comme la peste (ou le COVID19).


 
Pire. Pour celles et ceux qui ne testeraient que le centre de l’hexagone (le code du domaine) en bouchonnant vos adaptateurs, vous passeriez à côté de pleins de code et de bugs potentiels qui se trouveraient nichés dans vos adaptateurs non testés. 

Il est en effet extrêmement facile de se bercer d’illusion en mettant plein d’implicites dans le comportement de vos stubs qui vont faire office d’adaptateurs. Vous y mettrez très souvent ce que vous pensez que les adaptateurs feront ou doivent faire sans pour autant vous donner la garantie que cela sera le cas. Cela vous donnera pleins de tests verts, mais avec des comportements potentiellement buggés en prod (quand vous utiliserez les vrais adaptateurs concrets).


Quelle stratégie de test au fait ?


J’ai déjà écrit et parlé sur la question mais comme je n’ai pour l’instant pas traduit en anglais mon article le plus spécifique sur le sujet, je vais juste clarifier ici quelques termes indispensables pour comprendre la suite de mon article.

Après plus de 15 ans de pratique du TDD au boulot, je suis arrivé au fil des années à une forme économe d’outside-in TDD qui convient très bien à 90% de mes projets. Par Outside-in, je veux dire que je considère mon système (souvent une API web) dans son ensemble et depuis l’extérieur. Un peu comme une grosse boite noire que je vais faire grandir et améliorer progressivement en écrivant des tests d’acceptation qui la pilote. Ceux-ci ne vont pas s’intéresser aux détails d’implémentation (ce qui est fait et codé à l’intérieur de la boite) mais uniquement au comportement métier de celle-ci en interagissant avec elle depuis l’extérieur. Mes tests d’acceptation sont donc des tests gros grains, qui testent la boite noire (ici, l’ensemble de l’hexagone) mais qui ne font pas appel à des vrais protocoles techniques ni a de vrais data store pour être le plus rapide possible (je suis accro au feedback court et utilise pour ça un outil qui fait tourner mes tests à chaque fois que je change une ligne de code).

A côtés de ces tests d’acceptation qui constituent 80% de ma stratégie, j’écris en général quelques tests unitaires grains-fins (les petits tests de la « double loop » quand j'en ai besoin), quelques tests d’intégrations, et des tests de stubs ou de contrats vis à vis de mes dépendances externes (pour détecter quand ils changent des choses). Dans la suite de cet article, je ne parlerai que de mes tests d’acceptation.


Le meilleur des compromis possibles


Cette parenthèse sur les types de tests étant fermée, revenons à notre problème qui est d’éviter d’écrire des tests incomplets de notre système, et qui vous donneraient l’impression que tout va bien dans votre hexagone alors qu’au runtime vous aurez des mauvaises surprises. 

Ma recommandation : écrivez des tests d’acceptation qui couvrent tout votre hexagone, à l’exception des I/Os en bout de chaine. Oui, vous m’avez bien entendu : je vous conseille d’écrire des tests d’acceptations qui utilisent vos adaptateurs concrets et pas des stubs. La seule chose qu’il vous faudra bouchonner (stubber) du coup, ce sont les appels réseaux, les accès disques ou base de données qui eux, sont faits par vos adaptateurs. Ce faisant, vous testez tout votre code, en mode assemblage complet (sans illusion ni mauvaise surprise en prod donc), mais blazing fast ;-) 




C’est toujours plus clair avec un Example


Oui, prenons un Example pour être encore plus clair (déformation professionnelle ;-)

Imaginez que vous ayez à coder un générateur de fichiers sitemap.xml pour le SEO d’une plate-forme web au contenu complètement dynamique et contrôlée par plusieurs ligne métiers (e-biz, marketing). Cet hexagone pourrait être une web API utilisée quotidiennement à côté du site web, pour régénérer tous les fichiers sitemaps de celui-ci suite à un appel de type POST (HTTP) par exemple. PS: Par contre, ne dites pas à ma mère que j’ai parlé de micro-service dans un de mes articles ;-)

Bon, et bien au lieu d’écrire des tests d’acceptation comme ça :


Je vous conseille plutôt d’écrire des tests d’acceptation dans lesquels vous allez inclure vos adaptateurs concrets et uniquement stubber leurs I/Os, comme ça : 



 

Peut mieux faire

Bien sûr, pour avoir des tests d’acceptations lisibles, concis (pas plus de 10/15 lignes) et expressifs (c.a.d. en voyant bien les valeurs transmises à vos bouchons), vous allez en général passer par des builders pour vos stubs, mocks, voire même pour l’assemblage de votre hexagone. Mais cela aurait été moins clair ici, pour mon exemple. Une version cible pourrait ressembler à ça :

Passe-moi juste le plat stp...

Attention tout de même, le fait de tester ses adaptateurs ne doit pas vous empêcher de bien mettre votre logique ou orchestration métier au cœur de votre code métier (c.ad. Au cœur de l’hexagone). 

Un bon adaptateur est un adaptateur assez bête. Pour rappel, son rôle est de traduire un modèle lié à l’infrastructure vers un modèle métier et inversement + de faire le lien avec la technologie nécessaire. Il ne faut surtout pas arriver à une situation où un adaptateur utilise un autre adaptateur pour faire le malin ou optimiser certains appels. 

Dans un monde idéal, la complexité de votre système doit être essentielle, c.ad. Liée au métier et au problème à résoudre. A côté de cela, vous devez tout faire pour réduire la complexité accidentelle, c.ad. Technique de votre code d’infra. Des passe-plats à peine. Pas plus.


Avertissements

On pourra m’objecter ici que mes tests d’acceptation sont un peu hybrides et qu’ils couvrent à la fois des considérations métiers mais aussi certaines plus techniques. Pas très orthodoxe tout ça ;-) 

Bon déjà moi l’orthodoxie ou le by-the-book, je m’en fous. Et puis c’est complètement transparent si vous faites en sorte que chacun de vos tests d’acceptation couvrent bien des comportements métiers de votre système (service/API/micro-service... choisissez ici le nom approprié). A vous de travailler l’expressivité de vos tests : le nom de ceux-ci, la simplicité et la brièveté de l’assemblage et du « arrange » en mode DSL par exemple.

Avant d’arriver par tâtonnement à cette nouvelle stratégie (cette année), je mettais uniquement en œuvre la stratégie de test mise en avant par nos copains Londoniens de Cucumber (Seb Rose, Steve Tooke et Aslak Hellesøy). Celle-ci vise à combiner :

-         Énormément de tests d’acceptation mais pour lesquels on stubbe l’intégralité des adaptateurs (des tests supers rapides comme les miens donc) 

-         Et puis quelques tests complémentaires de contrats, dans un autre projet. Ceux-ci vérifiant que nos stubs d’adaptateurs utilisés côté acceptation ont exactement le même comportement que les vrais adaptateurs concrets qui sont eux assemblés et livrés avec notre hexagone en prod. Ces derniers tests d’intégration sont donc beaucoup plus lents et exécutés plus rarement (essentiellement sur l’usine de dev, pas dans mon Runner automatique local et permanent ncrunch) mais c’est un compromis volontaire.

On avait donc une situation vraiment intéressante qui permettait d’avoir pleins de tests d’acceptation supers rapides qu’on faisait tourner en permanence, et d’être suffisamment en confiance sur la capacité de nos stubs d’adaptateurs de droite pour nous aider à atteindre cet objectif (car on testait aussi qu’ils sont fidèles à la réalité dans quelques scenarios de référence). 

J'en parle au passé ici, car je n’ai pas réellement réussi à avoir suffisamment confiance dans ce dispositif dans mes expériences avec différentes équipes. Ce qui nous posait régulièrement problème avec cette stratégie telle-quelle, c’était l’effort moindre mise par toute l’équipe dans ces derniers tests de contrats. On arrivait encore plus souvent que d’habitude à des situations avec beaucoup trop de happy path (des scénarios de base où tout se passe bien) dans ces tests de contrats. Ceux-ci ne couvraient pas assez de cas d’erreurs ou d’exceptions. Pour le dire autrement, nos tests d’acceptations sollicitaient nos stubs sur beaucoup plus de cas que ce qui était prévu pour eux dans nos tests de contrat les concernant.

Alors c’est vrai que les devs que nous sommes sont attirés par les happy path autant que les papillons de nuit le sont par une ampoule allumée. Ça semble être un de nos déterminisme ;-) Mais j’ai un peu observé ces situations pour essayer de comprendre ce qui n’allait pas et en suis arrivé à la conclusion que c’etait parce que ces tests d’intégrations -en plus d’être très « orientés plomberie »-sont beaucoup plus lents à faire tourner. C’est pour ça que les gens y apportaient moins d’attention. Un peu en mode : "de toute façon c’est un test d’intégration pour un stub... ça doit faire le job mais on ne va pas y passer trop de temps non plus". 

La conséquence de tout ça ? On testait en général beaucoup moins de combinaisons de cas dans ces tests de contrats de nos stubs beaucoup que ce dont on avait l’habitude de faire dans nos tests unitaires grains fin ou dans nos tests d’acceptation gros grains.

Ceci avait pour effet que cette association des deux types de tests n’était pas suffisante, pour arriver à attraper tous les bugs ou les problèmes de plomberie dans notre assemblage final. C’est donc pour ces raisons que j’en suis finalement arrivé à la stratégie de test que je vous ai présentée dans cet article, et qui inclus les adaptateurs concrets dans les tests d’acceptation. Par contre, j’utilise toujours cette stratégie de test de contrats pour les composants extérieurs ou des APIs tierce, c’est juste que maintenant je stubbe moins de choses, mes stubs ne couvrent plus qu’une partie très fine.  

Comme dernier avertissement, je dois aussi préciser que ma stratégie de test fonctionne incroyablement bien dans mon contexte. Je n’ai en effet jamais rencontré de cas comme celui d’Alistair, où j’avais besoin d’exposer la même API avec plusieurs technologie différentes à gauche  (HTTP, AMQP, MQTT...). J’ai en général une exposition unique de mon domaine : du HTTP dans une web API, ou du Aeron dans un service low latency, ou du RPC dans... (non je rigole, je déteste trop le modèle RPC ;-)

Je n’ai donc besoin que d’un seul adaptateur côté gauche. Ce qui m’évite d’être confronté au fait de devoir rejouer autant de fois mes tests d’acceptation que je j’aurai de technologies d’exposition différentes. Ca vaut le coup de le rappeler ici, j’utilise surtout l’architecture hexagonale parce qu’elle me permet de bien séparer mon code métier de mon code technique. Pas pour pouvoir interchanger facilement ma technologie d'exposition

Je sais que ce côté « plugins pour attaquer la même logique métier via des technos différentes » est une des forces de l’architecture hexagonale, mais c’est un cas que je n’ai pour l’instant rencontré que dans des katas. Je ne suis d’ailleurs pas le seul dans ce cas, car dans toutes les formations DDD que j’ai pu donner ou lors de discussions dans des conférences, j’ai eu de nombreux témoignages de gens qui venaient le voir en me disant la même chose : « j’utilise mes contrôleurs web (de web API) comme adaptateur de gauche, car je n’ai pas le cas d’une multi-exposition à gauche ».

Un dernier bénéfice pour la route

Ce qui est sûr en revanche, c’est que le fait de tout tester -sauf les I/Os- vous mettra dans tous les cas dans une situation confortable car :
  • vos tests d’acceptation couvriront une code base très fidèle à la réalité de la prod
  • votre harnais de tests d’acceptation vous permettra de refactorer sereinement votre base de code dans le cas où vous vous seriez trompés et auriez mis du comportement métier dans vos adaptateurs. Avec ces tests qui couvrent l’ensemble, la move pourra être fait sans risque.

Peut-être pourrions-nous trouver un moyen facile (autre que le pair/code review) pour éviter aux personnes les moins expérimentées de tomber dans ce piège. Pour l’instant cette capacité de refactorer facilement a posteriori si mes collègues ou moi-même nous sommes trompés (ou avons pris un peu de dette technique court-terme), était largement suffisante sur mes projets. 


En conclusion

  1. Passez toujours par le centre de l’hexagone, ne connectez pas vos right-side adapters entre eux
  2. Ne codez pas de logique métier ni de logique d’orchestration dans vos adaptateurs
  3. Testez l’intégralité de votre hexagone (adaptateurs compris); ne bouchonnez que vos I/Os en bout de chaine utilisés par vos adaptateurs de droite

Les 2 prochains articles de cette série dédiée à l’architecture hexagonale parleront du sujet des health-checks (comment savoir si notre hexagone est en forme ou pas) mais aussi de la comparaison avec une variante du pattern, à savoir le Functional Core (imperative shell). A très vite.

Thomas