Blog

La dangerosité des mocks (2/2)

 Dans un premier article intitulé La dangerosité des mocks (1/2), je me suis efforcé de vous convaincre en quoi un mock pouvait être dangereux pour nos refactoring de code. Dans ce second article, je vous propose une solution alternative et j’en profite également pour égratigner encore les mocks au travers d’un exemple.

Quelle solution alors ?

Vous allez me dire, oui je veux bien ne pas utiliser de mock, mais alors, quelle solution je dispose pour bien tester mon service sans tester également une dépendance (mon fameux weatherService) ?

La solution : utiliser un fake ?

Un quoi ? Un fake, un bouchon si vous préférez !

Un bouchon (au sens test unitaire bien sûr) remplace l’implémentation du véritable service. Un bouchon implémente donc notre interface weatherService comme ci-dessous :

FakeWeatherService.V1

Notre bouchon implémente nos deux méthodes en utilisant une Map comme source de données fournie dans le constructeur. C’est donc à l’application appelante, ici notre classe de test, de fournir le jeu de données. Attention un bouchon ne doit pas être implémenté en fonction de nos cas de tests. En effet on peut remarquer que les deux méthodes bouchonnées sont indépendantes des jeux de données (d’où le passage d’une Map dans le constructeur du bouchon). Un bouchon n’est pas uniquement une simple classe, il peut réaliser de véritables traitements si on le souhaite. Selon moi un bouchon doit se rapprocher le plus possible de la véritable implémentation pour que nos tests soient pertinents.

Notre classe de test est remaniée comme ceci :

RainingServiceTest.V2

On crée directement notre jeu de données dans la méthode injectData annotée par @Before. La création du jeu de données aurait très bien pu se faire à partir d’une source externe, comme un simple fichier CSV par exemple. Le service weatherService (notre fake) est également instancié dans cette méthode. Remarquez comme les tests sont beaucoup plus simple, le code est beaucoup moins verbeux que lorsque l’on utilisait un mock. Il s’agit d’un avantage non négligeable par rapport à un mock, et ce n’est pas le seul.

Exécutons nos tests. Et voilà nos tests passent, on peut faire péter le bouchon (de champagne) !

Oui mais ….

C’est bien beau tout ça mais que se serait-il passé si on avait utilisé un fake dès le départ ? Aurions-nous eu les mêmes problèmes ?

Et bien non !! En effet, en ajoutant une nouvelle méthode dans le service weatherService, la classe bouchon FakeWeatherService n’aurait pas compilé. Je rappelle qu’avec un mock toutes les classes compilaient, seuls les tests ne passaient plus. C’est quand même un sacré avantage (un de plus) !

Du coup on aurait implémenté la nouvelle méthode dans la classe FakeWeatherService en essayant de le faire correctement sans erreur. On peut même envisager de faire un test unitaire sur la classe bouchon pour vérifier tout ça. Et c’est tout !!! Nous n’aurions même pas eu besoin de modifier nos tests unitaires : autre avantage.

Le seul inconvénient qui me vient à l’esprit dans cet exemple est que l’on doit maintenir la classe bouchon. Si notre code est bien fait, la classe ne doit pas comporter beaucoup de méthodes et chacune doit réaliser une seul chose. En effet comme une classe, une méthode ne doit avoir qu’une raison pour qu’elle change (principe SRP), mais il s’agit là d’un autre sujet.

C’est pire encore …

L’utilisation de mock peut également générer des faux positifs et des faux négatifs. Et oui ! Mais comment ?

Reprenons notre exemple.

Le service weatherService propose cette fois ci une nouvelle méthode qui nous retourne directement l’information souhaitée :

boolean isRaining( long code );

Dans la classe bouchon j’implémente donc cette nouvelle méthode que voici :

FakeWeatherService.V2

Dans la classe RainingService j’invoque alors la nouvelle méthode comme ceci

return weatherService.isRaining( city.getCode() );

J’exécute les tests unitaires sans les mocks, et tout se passe correctement. Je suis rassuré, je n’ai pas engendré une quelconque régression.

Maintenant exécutons nos tests unitaires avec les mocks sans les modifier. Et là surprise ! Le premier test sur la ville de Paris échoue (faux négatif) et celui sur la ville de Marseille réussit (faux positif). Mais que se passe-t-il ? C’est à devenir fou ! (je l’ai expérimenté).

Rappelez-vous, on a mocké la méthode byCityCode et pas la nouvelle méthode :

Mockito.when( weatherService.byCityCode( city.getCode() ) ).thenReturn( weather );

Or cette méthode n’est jamais invoquée par notre nouvelle classe RainingService. Nous n’avons pas indiqué au mock ce qu’il doit faire lorsque cette nouvelle méthode est invoquée. Il renvoie alors une valeur booléenne par défaut, c’est à dire false. Le test échoue mais c’est un faux positif. Pour que le test réussisse à nouveau il faut mocker la nouvelle méthode.

Pour le second test, même chose sauf que l’on s’attend à la valeur booléenne false. Donc il s’agit d’un test qui réussit mais un peu par chance on va dire.

Et après on s’étonne que les développeurs n’aiment pas faire des tests unitaires. Le développeur n’a alors plus confiance en ses tests unitaires. Résultat : il les laisse à l’abandon comme une vieille chaussette sale sur … (je m’égare).

Surtout que pour voir l’origine du problème il faut souvent déboguer l’exécution du test. Peut-être pas dans ce cas là qui est très simple, mais dans des cas plus complexes le débogage est absolument nécessaire.

Conclusion

Utiliser un bouchon est pour moi la solution au problème des mocks. Un bouchon permet de nous focaliser sur les interactions que l’on réalise avec l’objet testé et non pas comment l’implémentation est réalisé (boîte noire). C’est un des principes du TDD. On peut alors modifier l’implémentation tout en conservant intacts nos tests unitaires. L’utilisation d’un mock rend la mise en pratique du TDD tout simplement impossible.

Autre inconvénient des mocks dans les tests unitaires, ils ne sont pas fiables et peuvent vous faire passer des heures à trouver l’origine du problème. Dans certains cas ils peuvent même générer des faux positifs et des faux négatifs, chose improbable avec un bouchon.

Emeric Minodier

Written by

The author didnt add any Information to his profile yet