Comment dimensionnez-vous vos tests d'intégration?


21

J'étudie des techniques et des stratégies pour faire évoluer notre nombre croissant de tests d'intégration sur notre produit actuel, afin qu'ils puissent (humainement) faire partie de notre développement et du processus CI.

À environ 200+ tests d'intégration, nous atteignons déjà la marque de 1 heure pour terminer un test complet (sur une machine de développement de bureau), et cela affecte négativement la capacité d'un développeur à tolérer l'exécution de l'ensemble de la suite dans le cadre des processus de push de routine. Ce qui affecte la motivation à être discipliné pour bien les créer. Nous testons l'intégration uniquement des scénarios clés d'avant en arrière, et nous utilisons un environnement qui reflète la production, qui est construit à partir de zéro à chaque exécution de test.

En raison du temps qu'il faut pour exécuter, cela crée une boucle de rétroaction terrible et de nombreux cycles gaspillés en attente sur les machines pour terminer les tests, quelle que soit la concentration des tests. Peu importe l'impact négatif plus cher sur les flux et les progrès, la santé mentale et la durabilité.

Nous prévoyons d'avoir 10 fois plus de tests d'intégration avant que ce produit ne commence à ralentir (aucune idée vraiment, mais il ne semble pas que nous commencions encore en termes de fonctionnalités). Nous devons raisonnablement nous attendre à être dans les quelques centaines ou quelques milliers de tests d'intégration, je pense à un moment donné.

Pour être clair, pour essayer d'empêcher que cela ne devienne une discussion sur les tests unitaires par rapport aux tests d'intégration (qui ne devraient jamais être échangés). Nous effectuons les tests unitaires avec TDD ET les tests d'intégration dans ce produit. En fait, nous faisons des tests d'intégration aux différentes couches de l'architecture de services que nous avons, là où cela a du sens pour nous, car nous devons vérifier où nous introduisons des changements de rupture lors du changement des modèles de notre architecture vers les autres domaines de la système. 

Un peu sur notre pile technologique. Nous testons actuellement un environnement d'émulation (gourmand en CPU et en mémoire) pour exécuter nos tests de bout en bout. Composé de services Web Azure REST avec un backend noSql (ATS). Nous simulons notre environnement de production en exécutant l'émulateur de bureau Azure + IISExpress. Nous sommes limités à un émulateur et un référentiel backend local par machine de développement.

Nous avons également un CI basé sur le cloud, qui exécute le même test dans le même environnement émulé, et les cycles de test prennent deux fois plus de temps (2 heures +) dans le cloud avec notre fournisseur de CI actuel. Nous avons atteint les limites du SLA des fournisseurs de CI cloud en termes de performances matérielles, et dépassé leur allocation lors de l'exécution des tests. Pour être juste envers eux, leurs spécifications ne sont pas mauvaises, mais elles sont clairement la moitié d'une machine de bureau grunty interne.

Nous utilisons une stratégie de test consistant à reconstruire notre magasin de données pour chaque groupe logique de tests et à précharger les données de test. Tout en assurant l'intégrité complète des données, cela ajoute un impact de 5 à 15% sur chaque test. Nous pensons donc qu'il y a peu à gagner à optimiser cette stratégie de test à ce stade du développement du produit. 

Le long et le court de celui-ci est que: alors que nous pouvions optimiser le débit de chaque test (même si jusqu'à 30% -50% chacun), nous n'évoluerons toujours pas efficacement dans un proche avenir avec plusieurs centaines de tests. 1 heure est maintenant encore bien au-delà de ce qui est humainement tolérable, nous avons besoin d'un ordre de grandeur d'amélioration du processus global pour le rendre durable.

Donc, j'étudie quelles techniques et stratégies nous pouvons utiliser pour réduire considérablement le temps de test.

  • Écrire moins de tests n'est pas une option. Permet de ne pas débattre de celui-là dans ce fil.
  • L'utilisation d'un matériel plus rapide est certainement une option, bien que très coûteuse.
  • Exécuter des groupes de tests / scénarios sur du matériel séparé en parallèle est également une option préférée.
  • La création d'un regroupement de tests autour de fonctionnalités et de scénarios en cours de développement est plausible, mais en fin de compte pas fiable pour prouver une couverture complète ou la certitude que le système n'est pas affecté par un changement. 
  • L'exécution dans un environnement de transfert à l'échelle du cloud au lieu de s'exécuter dans l'émulateur de bureau est techniquement possible, bien que nous commencions à ajouter des temps de déploiement aux tests (~ 20 minutes chacun au début du test pour déployer les choses).
  • La division des composants du système en pièces logiques indépendantes est plausible dans une certaine mesure, mais nous nous attendons à un kilométrage limité à ce sujet, car les interactions entre les composants devraient augmenter avec le temps. (c.-à-d. qu'un changement est en cours est susceptible d'affecter les autres de manière inattendue - comme cela arrive souvent lorsqu'un système est développé progressivement)

Je voulais voir quelles stratégies (et outils) les autres utilisent dans cet espace.

(Je dois croire que d'autres peuvent rencontrer ce genre de difficulté à utiliser certains ensembles technologiques.))

[Mise à jour: 16/12/2016: Nous avons fini par investir davantage dans les tests parallèles CI, pour une discussion des résultats: http://www.mindkin.co.nz/blog/2015/12/16/16-jobs]


Depuis la rédaction de ce post, j'ai étudié que nCrunch (que nous utilisons largement pour nos tests unitaires) pourrait être un outil qui pourrait nous offrir une tactique. De toute évidence, il a la capacité d'expédier des tests vers des machines distantes et de les exécuter en parallèle. Donc, identifier des groupes de tests d'intégration, ainsi que plusieurs instances de machines cloud hautement spécifiées pourrait être une chose à essayer? nCrunch affirme que c'est exactement l'intention de cette capacité. Quelqu'un d'autre a essayé ça?
Jezz Santos

On dirait que cela descend dans une discussion sur ce qui est et ce qui n'est pas un test d'intégration, et sur la mauvaise compréhension des tests unitaires et des tests d'intégration, oh boy!
Jezz Santos

Réponses:


9

J'ai travaillé dans un endroit qui a pris 5 heures (sur 30 machines) pour exécuter des tests d'intégration. J'ai refactorisé la base de code et fait des tests unitaires à la place pour les nouveaux trucs. Les tests unitaires ont duré 30 secondes (sur 1 machine). Oh, et les bugs ont également baissé. Et le temps de développement puisque nous savions exactement ce qui rompait avec les tests granulaires.

Pour faire court, vous ne le faites pas. Les tests d'intégration complète augmentent de façon exponentielle à mesure que votre base de code croît (plus de code signifie plus de tests et plus de code signifie que tous les tests prennent plus de temps à exécuter car il y a plus d '«intégration» à exécuter). Je dirais que tout ce qui se trouve dans la plage "heures" perd la plupart des avantages de l'intégration continue car la boucle de rétroaction n'est pas là. Même un ordre d'amplitude ne suffit pas pour vous améliorer - et il est loin d'être extensible.

Je recommanderais donc de réduire les tests d'intégration aux tests de fumée les plus larges et les plus vitaux. Ils peuvent ensuite être exécutés tous les soirs ou à un intervalle moins que continu, réduisant ainsi une grande partie de votre besoin de performances. Les tests unitaires, qui ne se développent que de façon linéaire lorsque vous ajoutez du code (les tests augmentent, le temps d'exécution par test ne le fait pas) sont la voie à suivre pour l'échelle.


Je suis d'accord. Les tests unitaires sont beaucoup plus évolutifs et prennent en charge une boucle de rétroaction plus rapide.
Brandon

8
Vous avez peut-être manqué ce point. L'OP effectue déjà de nombreux tests uint ainsi que les tests d'intégration en question. Les tests unitaires ne remplacent jamais les tests d'intégration. Outil différent, pratiques différentes, objectifs différents, résultats différents. Il ne s'agit jamais de l'un ou de l'autre.
Jezz Santos

1
Ajout de clarté à l'article pour indiquer clairement que nous construisons ce produit à l'aide de TDD, nous avons donc déjà des milliers de tests unitaires, soutenus par les tests d'intégration en question. .
Jezz Santos

8

Les tests d'intégration seront toujours longs car ils devraient imiter un véritable utilisateur. Pour cette raison, vous ne devriez pas les exécuter tous de manière synchrone!

Étant donné que vous exécutez déjà des choses dans le cloud, il me semble que vous êtes dans une position privilégiée pour faire évoluer vos tests sur plusieurs machines.

Dans le cas extrême, faites tourner un nouvel environnement par test et exécutez-les tous en même temps. Vos tests d'intégration ne prendront alors que le temps du test le plus long.


Bonne idée! en regardant une stratégie comme ça, mais avec quelques outils qui aident les tests distribués
Jezz Santos

4

Réduire / optimiser les tests me semble être la meilleure idée, mais dans le cas où ce n'est pas une option, j'ai une alternative à proposer (mais nécessite la construction de simples outils propriétaires).

J'ai rencontré un problème similaire mais pas dans nos tests d'intégration (ceux-ci ont fonctionné en quelques minutes). Au lieu de cela, c'était simplement dans nos versions: la base de code C à grande échelle, prendrait des heures à construire.

Ce que j'ai vu comme extrêmement inutile, c'est le fait que nous reconstruisions le tout à partir de zéro (environ 20 000 fichiers source / unités de compilation) même si seulement quelques fichiers source ont changé, et donc passer des heures pour un changement qui ne devrait prendre que quelques secondes ou minutes au pire.

Nous avons donc essayé la liaison incrémentielle sur nos serveurs de build, mais ce n'était pas fiable. Cela donnait parfois de faux négatifs et échouait à s'appuyer sur certains commits, pour ensuite réussir une reconstruction complète. Pire, cela donnait parfois des faux positifs et rapportait un succès de build, uniquement pour le développeur de fusionner une build cassée dans la branche principale. Nous avons donc recommencé à tout reconstruire à chaque fois qu'un développeur a poussé les modifications de sa branche privée.

Je détestais tellement ça. Je pénétrais dans les salles de conférence avec la moitié des développeurs jouant à des jeux vidéo et tout simplement parce qu'il n'y avait pas grand-chose d'autre à faire en attendant les versions. J'ai essayé d'obtenir un avantage de productivité en multitâche et en démarrant une nouvelle branche une fois que je me suis engagé afin de pouvoir travailler sur le code en attendant les builds, mais quand un test ou une build échouait, il devenait trop pénible de mettre en file d'attente les modifications après ce point et essayez de tout réparer et de tout recoudre.

Projet parallèle en attendant, intégrez plus tard

Donc, ce que j'ai fait à la place, c'était de créer un cadre squelettique de l'application - le même type d'interface utilisateur de base et les parties pertinentes du SDK pour moi de développer contre un projet distinct dans son ensemble. Ensuite, j'écrirais du code indépendant contre cela en attendant les builds, en dehors du projet principal. Cela m'a au moins donné un peu de codage pour que je puisse rester quelque peu productif, puis je commencerais à intégrer ce travail effectué complètement en dehors du produit dans le projet plus tard - des extraits de code latéraux. C'est une stratégie pour vos développeurs s'ils attendent beaucoup.

Analyser manuellement les fichiers source pour savoir quoi reconstruire / réexécuter

Pourtant, je détestais la façon dont nous perdions tant de temps à tout reconstruire tout le temps. J'ai donc pris sur moi quelques week-ends pour écrire du code qui analyserait les fichiers et ne reconstruirait que les projets pertinents - toujours une reconstruction complète, pas de lien incrémentiel, mais seulement des projets qui doivent être reconstruits ( dont les fichiers dépendants, analysés récursivement, ont changé). C'était totalement fiable et après l'avoir démontré et testé de manière exhaustive, nous avons pu utiliser cette solution. Cela a réduit le temps de construction moyen de quelques heures à quelques minutes car nous ne reconstruisions que les projets nécessaires (bien que les modifications du SDK central puissent encore prendre une heure, mais nous l'avons fait beaucoup moins souvent que les modifications localisées).

La même stratégie devrait être applicable aux tests d'intégration. Il suffit d'analyser récursivement les fichiers source pour déterminer de quels fichiers dépendent les tests d'intégration (ex: importen Java,#includeen C ou C ++) côté serveur, et les fichiers inclus / importés à partir de ces fichiers et ainsi de suite, création d'un graphique de fichier de dépendance d'inclusion / importation complet pour le système. Contrairement à l'analyse de build qui forme un DAG, le graphique ne doit pas être orienté car il s'intéresse à tout fichier modifié contenant du code pouvant être exécuté indirectement *. Réexécutez le test d'intégration uniquement si l'un des fichiers du graphique du test d'intégration concerné a changé. Même pour des millions de lignes de code, il était facile d'effectuer cette analyse en moins d'une minute. Si vous avez des fichiers autres que le code source qui peuvent affecter un test d'intégration, comme les fichiers de contenu, vous pouvez peut-être écrire des métadonnées dans un commentaire dans le code source indiquant ces dépendances dans les tests d'intégration, de sorte que si ces fichiers externes changent, les tests se relancer.

* À titre d'exemple, si test.c inclut foo.h qui est également inclus par foo.c, une modification de test.c, foo.h ou foo.c devrait marquer le test intégré comme nécessitant une nouvelle exécution.

Cela peut prendre une journée ou deux pour programmer et tester, en particulier dans l'environnement formel, mais je pense que cela devrait fonctionner même pour les tests d'intégration et cela vaut la peine si vous n'avez pas d'autre choix que d'attendre dans les heures pour les builds pour terminer (soit en raison du processus de construction ou de test ou d'emballage ou autre). Cela peut se traduire par autant d'heures de travail perdues en seulement quelques mois, ce qui réduirait le temps nécessaire à la construction de ce type de solution propriétaire, en plus de tuer l'énergie de l'équipe et d'augmenter le stress causé par les conflits lors de fusions plus importantes, faites moins souvent à cause de tout le temps perdu à attendre. C'est juste mauvais pour l'équipe dans son ensemble quand ils passent une grande partie de leur temps à attendre les choses.tout être reconstruit / réexécuté / reconditionné à chaque petit changement.


3

On dirait que vous avez beaucoup trop de tests d'intégration. Rappel de pyramide de test . Les tests d'intégration appartiennent au milieu.

À titre d'exemple prendre un dépôt avec méthode set(key,object), get(key). Ce référentiel est largement utilisé dans votre base de code. Toutes les méthodes qui dépendent de ce référentiel seront testées avec un faux référentiel. Désormais, vous n'avez besoin que de deux tests d'intégration, un pour set et un pour get.

Certains de ces tests d'intégration pourraient probablement être convertis en tests unitaires. Par exemple, les tests de bout en bout à mon avis devraient uniquement tester que le site est correctement configuré avec la chaîne de connexion correcte et les domaines corrects.

Les tests d'intégration doivent vérifier que l'ORM, les référentiels et les abstractions de file d'attente sont corrects. En règle générale, aucun code de domaine n'est nécessaire pour les tests d'intégration - uniquement des abstractions.

Presque tout le reste peut être testé à l'unité avec des implémentations tronquées / simulées / truquées / in-mem pour les dépendances.


1
Perspective intéressante. Nos tests d'intégration n'essaient pas de vérifier chaque permutation de chaque paramètre de chaque appel ReST. À notre avis, il ne s'agit pas de tests d'intégration. Ils exécutent des scénarios clés de bout en bout via l'API qui, à leur tour, ont touché divers magasins principaux et d'autres systèmes. Le but est de s'assurer qu'au fur et à mesure que l'API change, ils identifient les scénarios qui nécessitent une attention (c'est-à-dire ne fonctionnent plus comme prévu).
Jezz Santos

1
Nous avons des tests d'intégration à différents niveaux de l'architecture. Dans votre exemple, nous avons des tests unitaires pour les classes qui accèdent au magasin de données afin que nous sachions qu'ils font les bons appels à notre magasin de données, nous avons des tests d'intégration pour configurer une copie de nos magasins et tester qu'ils lisent et écrivent les données correctement avec le magasin. Ensuite, nous utilisons ces classes de données dans une API REST, que nous créons avec des tests unitaires, puis des tests d'intégration qui démarrent le service Web et appellent pour s'assurer que les données arrivent de l'arrière vers l'avant et vice versa. Suggérez-vous que nous avons trop de tests ici?
Jezz Santos

J'ai mis à jour ma réponse en réponse à vos commentaires.
Esben Skov Pedersen

2

D'après mon expérience dans un environnement Agile ou DevOps où les pipelines de livraison continue sont courants, les tests d'intégration doivent être effectués à mesure que chaque module est terminé ou ajusté. Par exemple, dans de nombreux environnements de pipeline de distribution continue, il n'est pas rare d'avoir plusieurs déploiements de code par développeur et par jour. L'exécution d'un ensemble rapide de tests d'intégration à la fin de chaque phase de développement avant le déploiement devrait être une pratique standard dans ce type d'environnement. Pour plus d'informations, un excellent eBook à inclure dans votre lecture sur ce sujet est un guide pratique des tests dans DevOps , écrit par Katrina Clokie.

Pour tester efficacement de cette manière, le nouveau composant doit être testé par rapport aux modules terminés existants dans un environnement de test dédié ou par rapport aux stubs et pilotes. En fonction de vos besoins, il est généralement judicieux de conserver une bibliothèque de modules de remplacement et de pilotes pour chaque module d'application dans un dossier ou une bibliothèque afin de permettre une utilisation rapide des tests d'intégration répétitifs. Garder les modules de remplacement et les pilotes organisés de cette façon facilite les modifications itératives, les maintient à jour et fonctionne de manière optimale pour répondre à vos besoins de test continus.

Une autre option à considérer est une solution initialement développée vers 2002, appelée Service Virtualization. Cela crée un environnement virtuel, simulant l'interaction du module avec les ressources existantes à des fins de test dans un DevOps d'entreprise complexe ou l'environnement Agile.

Cet article peut être utile pour mieux comprendre comment effectuer des tests d'intégration dans l'entreprise


Bien que cela puisse fonctionner (si le système peut être divisé en de tels modules, mais pas tous les produits le peuvent) - c'était la norme il y a quelque temps, cela retarde efficacement l'intégration, perdant ainsi tous les avantages de CI / CD. Un peu contre-agile, tu ne crois pas? Les problèmes découverts dans de tels tests d'intégration ne peuvent pas être facilement et rapidement mis en correspondance avec une validation particulière, donc nécessitent des investigations complètes, à partir de zéro, tout comme les bogues provenant de la production (et vous savez combien plus coûteux à corriger).
Dan Cornilescu

1

Avez-vous mesuré chaque test pour voir où le temps est pris? Et puis, mesuré les performances de la base de code s'il y a un bit particulièrement lent. Le problème global est-il l'un des tests ou du déploiement, ou les deux?

En règle générale, vous souhaitez réduire l'impact du test d'intégration afin de les exécuter sur des modifications relativement mineures. Ensuite, vous pouvez quitter le test complet pour une exécution «QA» que vous effectuez lorsque la branche est promue au niveau suivant. Vous disposez donc de tests unitaires pour les branches de développement, exécutez des tests d'intégration réduits lors de la fusion et exécutez un test d'intégration complet lors de la fusion vers une branche candidate de publication.

Cela signifie donc que vous n'avez pas à reconstruire et à reconditionner et à redéployer tout à chaque validation. Vous pouvez organiser votre configuration, dans l'environnement de développement, pour effectuer un déploiement aussi bon marché que possible, en vous assurant qu'il sera OK. Au lieu de faire tourner une VM entière et de déployer le produit entier, laissez la VM avec l'ancienne version en place et copiez de nouveaux binaires en place, par exemple (YMMV selon ce que vous devez faire).

Cette approche optimiste globale nécessite toujours le test complet, mais cela peut être effectué à un stade ultérieur lorsque le temps pris est moins urgent. (Par exemple, vous pouvez exécuter le test complet une fois pendant la nuit, s'il y a des problèmes, le développeur peut les résoudre le matin). Cela a également l'avantage de rafraîchir le produit sur la plate-forme d'intégration pour les tests du lendemain - il peut devenir obsolète lorsque les développeurs changent les choses, mais seulement d'un jour.

Nous avons rencontré un problème similaire lors de l'exécution d'un outil d'analyse statique basé sur la sécurité. Les exécutions complètes prendraient du temps, nous avons donc déplacé l'exécution des validations du développeur vers une validation d'intégration (c'est-à-dire que nous avions un système où le développeur a dit qu'elles étaient terminées, il a été fusionné dans une branche de `` niveau 2 '' où plus de tests ont été effectués, y compris perf tests. Une fois terminé, il a été fusionné à une branche QA pour le déploiement. L'idée est de supprimer les exécutions régulières qui se produiraient en continu aux exécutions effectuées tous les soirs - les développeurs obtiendraient les résultats le matin et n'affecteraient pas leur développement. se concentrer plus tard dans leur cycle de développement).


1

À un moment donné, un ensemble complet de tests d'intégration peut prendre plusieurs heures, même sur un matériel coûteux. L'une des options consiste à ne pas exécuter la majorité de ces tests sur chaque validation, et à les exécuter tous les soirs, ou en mode batch continu (une fois par plusieurs validations).

Cependant, cela crée un nouveau problème - les développeurs ne reçoivent pas de commentaires immédiats et les versions cassées peuvent passer inaperçues. Pour résoudre ce problème, il est important qu'ils sachent que quelque chose est cassé à tout moment. Construire des outils de notification comme Catlight ou le notificateur de plateau de TeamCity peut être très utile.

Mais il y aura encore un autre problème. Même lorsque le développeur constate que la version est rompue, il ne se précipite peut-être pas pour la vérifier. Après tout, quelqu'un d'autre peut déjà le vérifier, non?

Pour cette raison, ces deux outils ont une fonction "build investigation". Il dira si quelqu'un de l'équipe de développement vérifie et corrige réellement la version cassée. Les développeurs peuvent se porter volontaires pour vérifier la construction et, jusqu'à ce que cela se produise, tout le monde dans l'équipe sera ennuyé par une icône rouge près de l'horloge.


0

Il semble que votre base de code s'agrandisse, et une gestion du code vous aidera. Nous utilisons Java, donc excuses à l'avance si je suppose.

  • Un grand projet doit être décomposé en petits projets individuels qui se compilent dans les bibliothèques. Les outils Java comme nexus vous facilitent la tâche.
  • Chaque bibliothèque doit implémenter une interface. Cela facilite le blocage de la bibliothèque dans les tests de niveau supérieur. Ceci est particulièrement utile si la bibliothèque accède à une base de données ou à une banque de données externe (par exemple un mainframe). Dans de tels cas, l'obtention des données de l'ordinateur central ou de la base de données dans un état répétable sera probablement lente et peut être impossible.
  • Les tests d'intégration pour chaque bibliothèque peuvent être complets, mais ne doivent être exécutés que lorsque la nouvelle source de bibliothèque est validée.
  • Les tests d'intégration de niveau supérieur doivent simplement appeler les bibliothèques et supposer qu'elles sont parfaites.

La boutique Java dans laquelle je travaille utilise cette approche et nous attendons rarement que les tests d'intégration s'exécutent.


Merci, mais je pense que nous n'avons pas la même compréhension de l'objectif et de l'application des tests d'intégration dans ce contexte. Vous pouvez confondre les tests d'intégration avec les tests unitaires.
Jezz Santos

0

Une autre approche possible pour conserver les tests d'intégration de pipeline CI (ou tout type de vérification, y compris les builds) avec des temps d'exécution longs ou nécessitant des ressources limitées et / ou coûteuses consiste à passer des systèmes CI traditionnels basés sur des vérifications post-validation (qui sont sensibles à la congestion ) à une basée sur des vérifications préalables à la validation .

Au lieu d'engager directement leurs modifications dans la succursale, les développeurs les soumettent à un système de vérification automatisé centralisé qui effectue les vérifications et:

  • en cas de succès, il valide automatiquement les modifications dans la branche
  • en cas d'échec, il informe les soumissionnaires respectifs de réévaluer leurs modifications

Une telle approche permet de combiner et de tester ensemble plusieurs modifications soumises, augmentant potentiellement la vitesse de vérification CI efficace plusieurs fois.

Un tel exemple est le système de porte basé sur Gerrit / Zuul utilisé par OpenStack .

Un autre est ApartCI ( avertissement - je suis son créateur et le fondateur de la société qui le propose).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.