Comment créer une pyramide dans le tronc ou les applications de développement piloté par les tests sur Spring Boot

Le framework Spring est souvent cité comme exemple du framework Cloud Native , conçu pour fonctionner dans le cloud, développer des applications à douze facteurs , des microservices et l'un des produits les plus stables, mais en même temps innovants. Mais dans cet article, je voudrais m'attarder sur un autre aspect fort de Spring: c'est son support au développement par le biais de tests (capacité TDD?). Malgré la connectivité TDD, j'ai souvent remarqué que les projets Spring ignoraient certaines des meilleures pratiques pour les tests, inventaient leurs propres vélos ou n'écrivaient pas du tout parce qu'ils étaient "lents" ou "peu fiables". Et je vais vous dire exactement comment écrire des tests rapides et fiables pour les applications sur Spring Framework et effectuer le développement par le biais de tests. Donc, si vous utilisez Spring (ou voulez commencer), comprenez quels sont les tests en général (ou voulez comprendre), ou pensez que contextLoads est le niveau nécessaire et suffisant de test d'intégration - ce sera intéressant!


La capacité TDD est très ambiguë et mal mesurable, mais Spring a néanmoins beaucoup de choses qui, par conception, aident à écrire l'intégration et les tests unitaires avec un minimum d'effort. Par exemple:


  • Test d'intégration - vous pouvez facilement lancer l'application, verrouiller les composants, redéfinir les paramètres, etc.
  • Concentrez les tests d'intégration - uniquement l'accès aux données, uniquement le Web, etc.
  • Support prêt à l'emploi - bases de données en mémoire, files d'attente de messages, authentification et autorisation dans les tests
  • Test via des contrats (Spring Cloud Contract)
  • Prise en charge des tests de l'interface utilisateur Web à l'aide de HtmlUnit
  • Flexibilité de la configuration de l'application - profils, configurations de test, composants, etc.
  • Et bien plus

Pour commencer, une petite mais nécessaire introduction sur TDD et les tests en général.


Développement piloté par les tests


TDD est basé sur une idée très simple - nous écrivons des tests avant d' écrire du code. En théorie, cela semble effrayant, mais après un certain temps, une compréhension des pratiques et des techniques vient, et la possibilité de passer des tests par la suite provoque un inconfort tangible. L'une des pratiques clés est l' itération , c'est-à-dire faire tout petit, des itérations ciblées, dont chacune est décrite comme un Red-Green-Refactor .


Dans la phase rouge , nous écrivons un test qui tombe, et il est très important qu'il tombe avec une raison et une description claires et compréhensibles, et que le test lui-même soit complet et réussit lorsque le code est écrit. Le test doit vérifier le comportement , pas l' implémentation , c'est-à-dire suivez l'approche de la boîte noire, puis je vais vous expliquer pourquoi.


Dans la phase verte , nous écrivons le code minimum nécessaire pour réussir le test. Parfois, il est intéressant de s'entraîner et de le rendre aussi fou que possible (même s'il vaut mieux ne pas s'emballer) et lorsqu'une fonction retourne un booléen selon l'état du système, le premier "pass" peut simplement être return true .


Dans la phase de refactoring , qui ne peut être lancée que lorsque tous les tests sont verts , nous allons refactoriser le code et le remettre en bon état. Ce n'est même pas nécessaire pour un morceau de code que nous avons écrit, il est donc important de commencer la refactorisation sur un système stable. L'approche de la «boîte noire» aidera simplement à refactoriser, à changer l'implémentation, mais sans toucher au comportement.


Je parlerai des différents aspects du TDD à l'avenir, après tout, c'est l'idée d'une série d'articles, donc maintenant je ne m'attarderai pas particulièrement sur les détails. Mais avant de répondre aux critiques standard de TDD, je mentionnerai quelques mythes que j'entends souvent.


  • "TDD représente une couverture à 100% du code, mais il ne donne aucune garantie" - le développement par le biais de tests n'a aucun rapport avec une couverture à 100%. Dans de nombreuses équipes où je travaillais, cette métrique n'était même pas mesurée et était classée comme métrique de vanité. Et oui, une couverture de test à 100% ne veut rien dire.
  • "TDD ne fonctionne que pour des fonctions simples, une vraie application avec une base de données et un état difficile ne peut pas être créé avec elle" est une excuse très populaire, généralement complétée par "Nous avons une application tellement compliquée que nous n'écrivons pas du tout les tests, c'est impossible". J'ai vu une approche TDD fonctionnelle sur des applications complètement différentes - Web (avec et sans SPA), mobile, API, microservices, monolithes, systèmes bancaires complexes, plateformes cloud, frameworks, plateformes de vente au détail écrites dans différents langages et technologies. Ainsi, le mythe populaire «Nous sommes uniques, tout est différent» est le plus souvent une excuse pour ne pas investir des efforts et de l'argent dans les tests, mais pas une vraie raison (bien qu'il puisse également y avoir de vraies raisons).
  • «Il y aura toujours des bogues avec TDD» - bien sûr, comme dans tout autre logiciel. TDD ne concerne pas les bugs ou leur absence, c'est un outil de développement. Comme le débogage. Comme un IDE. Comme la documentation. Aucun de ces outils ne garantit l'absence de bogues, ils aident seulement à faire face à la complexité croissante du système.

L'objectif principal du TDD et des tests en général est de donner à l'équipe l' assurance que le système fonctionne de manière stable. Par conséquent, aucune des pratiques de test ne détermine le nombre et les tests à écrire. Écrivez combien vous pensez nécessaire, combien vous devez être sûr que le code peut être mis en production dès maintenant et qu'il fonctionnera . Il y a des gens qui considèrent les tests d'intégration rapide comme une boîte noire ultimative nécessaire et suffisante, et les tests unitaires facultatifs. Quelqu'un dit que les tests e2e avec la possibilité d'un retour rapide à la version précédente et la présence de versions canaries ne sont pas si critiques. Combien d'équipes - tant d'approches, il est important de trouver la vôtre.

L'un de mes objectifs est de m'éloigner du format «développement en testant une fonction qui ajoute deux nombres» dans l'histoire de TDD et de regarder une application réelle, une sorte de pratique de test qui a été évaporée en une application minimale, collectée sur des projets réels. En tant qu'exemple semi-réel, je vais utiliser une petite application Web que j'ai moi-même inventée pour l'abstrait usines Boulangerie - Pâtisserie . Je prévois d'écrire de petits articles, en me concentrant à chaque fois sur un élément distinct des fonctionnalités de l'application et en montrant à travers TDD que vous pouvez concevoir des API, la structure interne de l'application et maintenir une refactorisation constante.


Voici un exemple de plan pour une série d'articles, tel que je le vois actuellement:


  1. Walking skeleton - cadre d'application où vous pouvez exécuter le cycle Red-Green-Refactor
  2. Test d'interface utilisateur et conception basée sur le comportement
  3. Test d'accès aux données (Spring Data)
  4. Tests d'autorisation et d'authentification (Spring Security)
  5. Jet Stack (WebFlux + Project Reactor)
  6. Interopérabilité des (micro) services et contrats (Spring Cloud)
  7. Test de Message Queuing (Spring Cloud)

Cet article d'introduction portera sur les points 1 et 2 - je vais créer un cadre d'application et un test d'interface utilisateur de base en utilisant le BDD - ou une approche de développement axée sur le comportement . Chaque article débutera par une user story , mais je ne parlerai pas de la partie «produit» pour gagner du temps. La user story sera écrite en anglais, il sera bientôt clair pourquoi. Tous les exemples de code peuvent être trouvés sur GitHub, donc je n'analyserai pas tout le code, seulement les parties importantes.


La user story est une description d'une caractéristique d'une application en langage naturel qui est généralement écrite au nom d'un utilisateur du système.

User story 1: L'utilisateur voit la page d'accueil


Comme Alice, un nouvel utilisateur
Je veux voir une page d'accueil lors de la visite du site Web de Cake Factory
Pour que je sache quand Cake Factory est sur le point de se lancer

Critères d'acceptation:
Scénario: un utilisateur visitant le site Web avant la date de lancement
Étant donné que je suis un nouvel utilisateur
Quand je visite le site Web de Cake Factory
Ensuite, je vois un message «Merci pour votre intérêt»
Et je vois un message "Le site Web arrive bientôt ..."

Il faudra des connaissances: qu'est -ce que le développement basé sur le comportement et Cucumber , les bases du Spring Boot Testing .


La première user story est assez basique, mais l'objectif n'est pas encore dans la complexité, mais dans la création d' un squelette ambulant - une application minimale pour démarrer le cycle TDD .


Après avoir créé un nouveau projet sur Spring Initializr avec des modules Web et Moustache, pour commencer, j'aurai besoin de quelques modifications supplémentaires pour build.gradle :


  • ajoutez HtmlUnit testImplementation('net.sourceforge.htmlunit:htmlunit') . Vous n'avez pas besoin de spécifier la version, le plugin de gestion des dépendances Spring Boot pour Gradle sélectionnera automatiquement la version nécessaire et compatible
  • migrer un projet de JUnit 4 vers JUnit 5 (car 2018 est dans la cour)
  • ajouter des dépendances à Cucumber - une bibliothèque que j'utiliserai pour écrire les spécifications BDD
  • supprimer créé par défaut CakeFactoryApplicationTests avec inévitable contextLoads

En gros, c'est le "squelette" de base de l'application, vous pouvez déjà écrire le premier test.


Pour faciliter la navigation dans le code, je parlerai brièvement des technologies utilisées.


Concombre


Cucumber est un cadre de développement axé sur le comportement qui aide à créer des "spécifications exécutables", c'est-à-dire exécuter des tests (spécifications) écrits en langage naturel. Le plugin Cucumber analyse le code source en Java (et dans de nombreux autres langages) et utilise des définitions d'étapes pour exécuter du code réel. Les définitions d'étape sont des méthodes de classe annotées par @Given , @When , @Then et d'autres annotations.


Htmlunit


La page d'accueil du projet appelle HtmlUnit "un navigateur sans interface graphique pour les applications Java". Contrairement à Selenium, HtmlUnit ne lance pas de véritable navigateur et, surtout, ne rend pas la page du tout, en travaillant directement avec le DOM. JavaScript est pris en charge via le moteur Mozilla Rhino. HtmlUnit est bien adapté aux applications classiques, mais pas très convivial avec les applications à page unique. Pour commencer, ce sera suffisant, puis j'essaierai de montrer que même des choses comme un framework de test peuvent faire partie de l'implémentation, et non le fondement de l'application.


Premier test


Maintenant, une histoire d'utilisateur écrite en anglais me sera utile. Le meilleur déclencheur pour démarrer la prochaine itération TDD est des critères d'acceptation écrits de telle manière qu'ils peuvent être transformés en une spécification exécutable avec un minimum de gestes.


Idéalement, les user stories doivent être écrites de manière à pouvoir être simplement copiées dans la spécification BDD et exécutées. C'est loin d'être toujours simple et pas toujours possible, mais cela devrait être l'objectif du propriétaire du produit et de toute l'équipe, bien qu'il ne soit pas toujours réalisable.

Donc, ma première fonctionnalité.


 Feature: Welcome page Scenario: a user visiting the web-site visit before the launch date Given a new user, Alice When she visits Cake Factory web-site Then she sees a message 'Thank you for your interest' And she sees a message 'The web-site is coming in December!' 

Si vous générez des descriptions d'étapes (le plugin Intellij IDEA aide à prendre en charge Gherkin aide beaucoup) et exécutez le test, alors, bien sûr, il sera vert - il ne teste encore rien. Et voici la phase importante du travail sur le test - vous devez écrire un test, comme si le code principal avait été écrit .


Souvent, pour ceux qui commencent à bannir le TDD, une stupeur s'installe ici - il est difficile de mettre en tête les algorithmes et la logique de quelque chose qui n'existe pas encore. Et par conséquent, il est très important d'avoir des itérations aussi petites et ciblées que possible, en commençant par la user story et en descendant jusqu'au niveau de l'intégration et de l'unité. Il est important de se concentrer sur un test à la fois et d'essayer de se mouiller et d'ignorer les dépendances qui ne sont pas encore importantes. J'ai parfois remarqué à quel point les gens s'écartent facilement - créer une interface ou une classe pour une dépendance, générer immédiatement une classe de test vide pour elle, une dépendance supplémentaire y est ajoutée, une autre interface est créée et ainsi de suite.


Si l'histoire est "il serait nécessaire de rafraîchir le statut lors de l'enregistrement", il est très difficile d'automatiser et de formaliser. Dans mon exemple, chaque étape peut être clairement présentée dans une séquence d'étapes qui peuvent être décrites par code. Il est clair que c'est l'exemple le plus simple et il ne montre pas grand-chose, mais j'espère que, avec une complexité croissante, il sera plus intéressant.

Rouge


Donc, pour ma première fonctionnalité, j'ai créé les descriptions d'étapes suivantes:


 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WelcomePage { private WebClient webClient; private HtmlPage page; @LocalServerPort private int port; private String baseUrl; @Before public void setUp() { webClient = new WebClient(); baseUrl = "http://localhost:" + port; } @Given("a new user, Alice") public void aNewUser() { // nothing here, every user is new by default } @When("she visits Cake Factory web-site") public void sheVisitsCakeFactoryWebSite() throws IOException { page = webClient.getPage(baseUrl); } @Then("she sees a message {string}") public void sheSeesAMessageThanksForYourInterest(String expectedMessage) { assertThat(page.getBody().asText()).contains(expectedMessage); } } 

Quelques points à prendre en compte:


  • les fonctionnalités sont lancées par un autre fichier, Features.java utilisant l'annotation RunWith de JUnit 4, Cucumber ne prend pas en charge la version 5, hélas
  • @SpringBootTest annotation @SpringBootTest est ajoutée à la description des étapes, cucumber-spring récupère à partir de là et configure le contexte de test (c.-à-d. Lance l'application)
  • L'application Spring pour le test commence par webEnvironment = RANDOM_PORT et ce port aléatoire est transmis au test à l'aide de @LocalServerPort , Spring trouvera cette annotation et définira la valeur du champ sur le port du serveur

Et le test, comme prévu, se bloque avec l'erreur 404 for http://localhost:51517 .


Les erreurs avec lesquelles le test se bloque sont extrêmement importantes, en particulier en ce qui concerne les tests unitaires ou d'intégration, et ces erreurs font partie de l'API. Si le test se bloque avec une NullPointerException ce n'est pas trop bon, mais la BaseUrl configuration property is not set - bien mieux.

Vert


Pour rendre le test vert, j'ai ajouté un contrôleur de base et une vue avec un minimum de HTML:


 @Controller public class IndexController { @GetMapping public String index() { return "index"; } } 

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Cake Factory</title> </head> <body> <h1>Thank you for your interest</h1> <h2>The web-site is coming in December!</h2> </body> </html> 

Le test est vert, l'application fonctionne, bien qu'elle soit faite dans la tradition d'une conception technique sévère.


Sur un vrai projet et dans une équipe équilibrée , bien sûr, je m'asseyais avec le designer et nous transformions le HTML nu en quelque chose de beaucoup plus beau. Mais dans le cadre de l'article, un miracle ne se produira pas, la princesse restera une grenouille.

La question «quelle partie de TDD est la conception» n'est pas si simple. L'une des pratiques que j'ai trouvées utiles est au début de ne même pas regarder du tout l'interface utilisateur (pas même d'exécuter l'application pour sauver vos nerfs), d'écrire un test, de le rendre vert - puis, ayant une base stable, de travailler sur le front-end, de redémarrer constamment les tests .


Refactor


Dans la première itération, il n'y a pas de refactoring particulier, mais bien que j'ai passé les 10 dernières minutes à choisir un modèle pour Bulma , qui peut être considéré comme du refactoring!


En conclusion


Alors que l'application n'a ni travail de sécurité, ni base de données, ni API, les tests et TDD semblent assez simples. Et en général, à partir de la pyramide des tests, je n'ai touché qu'au sommet, le test de l'interface utilisateur. Mais en cela, en partie, le secret de l'approche Lean est de tout faire en petites itérations, un composant à la fois. Cela permet de se concentrer sur les tests, de les rendre simples et de contrôler la qualité du code. J'espère que dans les articles suivants il y aura plus d'intéressant.


Les références



PS Le titre de l'article n'est pas aussi fou que cela puisse paraître au début, je pense que beaucoup l'ont déjà deviné. "Comment construire une pyramide dans votre démarrage" est une référence à la pyramide de test (je vous en dirai plus à ce sujet plus tard) et à Spring Boot, où démarrage en anglais britannique signifie également "tronc".

Source: https://habr.com/ru/post/fr431306/


All Articles