Je suis fermement convaincu de l'intérêt d'utiliser des tests qui vérifient un programme complet (par exemple, des tests de convergence), y compris un ensemble automatisé de tests de régression . Après avoir lu quelques livres de programmation, j'ai eu le sentiment harcelant que je "devais" écrire des tests unitaires (c'est-à-dire, des tests qui vérifient la correction d'une seule fonction et ne constituent pas l'exécution du code complet pour résoudre un problème). . Cependant, les tests unitaires ne semblent pas toujours correspondre aux codes scientifiques et finissent par se sentir artificiels ou comme une perte de temps.
Devrions-nous écrire des tests unitaires pour les codes de recherche?
programming-paradigms
testing
David Ketcheson
la source
la source
Réponses:
Pendant de nombreuses années, j'ai eu la fausse impression que je n'avais pas assez de temps pour écrire des tests unitaires pour mon code. Lorsque j’écrivais des tests, ils étaient gonflés et constituaient des choses lourdes qui ne m’avaient incité à penser que je ne devrais écrire des tests unitaires que lorsque je savais que c’était nécessaire.
Ensuite, j'ai commencé à utiliser Test Driven Development et j'ai trouvé qu'il s'agissait d'une révélation complète. Je suis maintenant fermement convaincu que je n'ai pas le temps de ne pas écrire de tests unitaires .
D'après mon expérience, en développant en gardant à l'esprit les tests, vous obtenez des interfaces plus propres, des classes et des modules plus ciblés et généralement plus de code SOLID testable.
Chaque fois que je travaille avec du code existant qui ne comporte pas de tests unitaires et que je dois tester manuellement quelque chose, je continue de penser "cela serait tellement plus rapide si ce code comportait déjà des tests unitaires". Chaque fois que je dois essayer d'ajouter une fonctionnalité de test unitaire au code avec un couplage élevé, je continue de penser "cela serait tellement plus facile s'il avait été écrit de manière découplée".
Comparer et opposer les deux stations expérimentales que je supporte. L'un existe depuis un certain temps et contient beaucoup de code hérité, tandis que l'autre est relativement nouveau.
Lors de l'ajout de fonctionnalités à l'ancien laboratoire, il est souvent nécessaire de passer au laboratoire et de passer de nombreuses heures à comprendre les implications de la fonctionnalité dont elles ont besoin et la manière dont je peux ajouter cette fonctionnalité sans affecter aucune des autres fonctionnalités. Le code n'est tout simplement pas configuré pour permettre les tests hors ligne, de sorte que tout doit être développé en ligne. Si j'essayais de développer hors ligne, je me retrouverais avec plus d' objets factices que ce qui serait raisonnable.
Dans les laboratoires les plus récents, je peux généralement ajouter des fonctionnalités en les développant hors ligne sur mon bureau, en ne dédaignant que les tâches immédiatement nécessaires, puis en ne passant que peu de temps au laboratoire, en réglant tous les problèmes non résolus. -ligne.
Pour plus de clarté, et depuis @ naught101 a demandé ...
J'ai tendance à travailler sur des logiciels de contrôle expérimental et d'acquisition de données, avec quelques analyses de données ad hoc. La combinaison de TDD et de contrôle de révision permet donc de documenter à la fois les modifications apportées au matériel de test sous-jacent et les exigences de collecte de données au fil du temps.
Même dans le cas de l’élaboration d’un code exploratoire, j’ai pu voir un avantage significatif à avoir des hypothèses codifiées, ainsi qu’à la possibilité de voir comment ces hypothèses évoluent dans le temps.
la source
Les codes scientifiques ont tendance à avoir des constellations de fonctions imbriquées plus souvent que les codes commerciaux sur lesquels j'ai travaillé, généralement en raison de la structure mathématique du problème. Donc, je ne pense pas que les tests unitaires pour des fonctions individuelles soient très efficaces. Cependant, je pense qu’il existe une classe de tests unitaires qui sont efficaces et qui sont encore très différents des tests complets du programme en ce sens qu’ils ciblent des fonctionnalités spécifiques.
Je viens de définir brièvement ce que je veux dire par ces types de tests. Le test de régression recherche les modifications du comportement existant (validé d'une manière ou d'une autre) lorsque des modifications sont apportées au code. Le test unitaire exécute un morceau de code et vérifie qu'il fournit le résultat souhaité en fonction d'une spécification. Ils ne sont pas si différents, car le test de régression initial était un test unitaire, car je devais déterminer que la sortie était valide.
Deux autres exemples de tests unitaires, issus de PyLith , sont la localisation de point, qui est une fonction simple qui permet de produire des résultats synthétiques, et la création de cellules cohésives à volume nul dans un maillage, qui implique plusieurs fonctions, mais s'adresse à une pièce circonscrite. fonctionnalité dans le code.
Il existe de nombreux tests de ce type, notamment des tests de conservation et de cohérence. L'opération n'est pas très différente de la régression (vous exécutez un test et vérifiez la sortie par rapport à une norme), mais la sortie standard provient d'une spécification plutôt que d'une exécution précédente.
la source
Depuis que j'ai entendu parler du développement piloté par les tests dans Code Complete, 2e édition , j'ai utilisé un framework de tests unitaires.dans le cadre de ma stratégie de développement, cela a considérablement augmenté ma productivité en réduisant le temps passé au débogage, car les différents tests que j'écris sont de diagnostic. Comme avantage supplémentaire, je suis beaucoup plus confiant dans mes résultats scientifiques et j'ai utilisé mes tests unitaires à plusieurs reprises pour défendre mes résultats. S'il y a une erreur dans un test unitaire, je peux généralement comprendre pourquoi assez rapidement. Si mon application se bloque et que tous mes tests unitaires réussissent, je fais une analyse de couverture de code pour voir quelles parties de mon code ne sont pas exercées, ainsi que de parcourir le code avec un débogueur pour localiser la source de l'erreur. Ensuite, j'écris un nouveau test pour m'assurer que le bogue reste corrigé.
La plupart des tests que j'écris ne sont pas des tests unitaires purs. Strictement définis, les tests unitaires sont censés exercer les fonctionnalités d'une fonction. Lorsque je peux tester facilement une seule fonction à l'aide de données fictives, je le fais. D'autres fois, je ne peux pas facilement me moquer des données dont j'ai besoin pour écrire un test qui exerce les fonctionnalités d'une fonction donnée. Je vais donc tester cette fonction avec d'autres dans un test d'intégration. Tests d'intégrationtester le comportement de plusieurs fonctions à la fois. Comme le souligne Matt, les codes scientifiques sont souvent une constellation de fonctions imbriquées, mais souvent, certaines fonctions sont appelées en séquence et des tests unitaires peuvent être écrits pour tester la sortie à des étapes intermédiaires. Par exemple, si mon code de production appelle cinq fonctions en séquence, j'écrirai cinq tests. Le premier test appelle uniquement la première fonction (il s'agit donc d'un test unitaire). Ensuite, le deuxième test appelle les première et deuxième fonctions, le troisième test appelle les trois premières fonctions, etc. Même si je pouvais écrire des tests unitaires pour chaque fonction de mon code, j'écrirais quand même des tests d'intégration, car des bugs peuvent survenir lorsque plusieurs composants modulaires d'un programme sont combinés. Enfin, après avoir écrit tous les tests unitaires et les tests d'intégration dont je pense avoir besoin, je Je vais emballer mes études de cas dans des tests unitaires et les utiliser pour des tests de régression, car je veux que mes résultats soient reproductibles. S'ils ne sont pas répétables et que j'obtiens des résultats différents, je veux savoir pourquoi. L'échec d'un test de régression n'est peut-être pas un problème réel, mais il va m'obliger à déterminer si les nouveaux résultats sont au moins aussi fiables que les anciens.
L'analyse des codes statiques, les débogueurs de mémoire et la compilation avec des indicateurs d'avertissement du compilateur pour détecter les erreurs simples et le code inutilisé sont également intéressants.
la source
D'après mon expérience, à mesure que la complexité des codes de recherche scientifique augmente, il est nécessaire d'adopter une approche très modulaire en matière de programmation. Cela peut être douloureux pour les codes avec une base importante et ancienne (
f77
n'importe qui?), Mais il est nécessaire d'aller de l'avant. Lorsqu'un module est construit autour d'un aspect spécifique du code (pour les applications CFD, pensez aux conditions aux limites ou à la thermodynamique), les tests unitaires sont très utiles pour valider la nouvelle implémentation et isoler les problèmes et les développements logiciels ultérieurs.Ces tests unitaires doivent correspondre à un niveau inférieur à la vérification du code (puis-je récupérer la solution analytique de mon équation d’onde?) Et à deux niveaux inférieurs à la validation du code (puis-je prédire les valeurs efficaces maximales de pointe dans mon écoulement turbulent), tout en veillant à ce que la programmation (les arguments sont-ils correctement passés, les pointeurs pointent-ils vers la bonne chose?) et le "calcul" (ce sous-programme calcule le coefficient de frottement. Si je saisis un ensemble de nombres et calcule la solution à la main, la routine produit-elle le même résultat? résultat?) sont corrects. Aller au-dessus de ce que les compilateurs peuvent détecter, c’est-à-dire des erreurs de syntaxe élémentaires.
Je le recommanderais certainement pour au moins certains modules cruciaux de votre application. Cependant, il faut bien se rendre compte que c'est extrêmement fastidieux et prend beaucoup de temps. Par conséquent, à moins de disposer d'une main-d'œuvre illimitée, je ne le recommanderais pas pour 100% d'un code complexe.
la source
Les tests unitaires de codes scientifiques sont utiles pour diverses raisons.
Trois en particulier sont:
Les tests unitaires aident les autres utilisateurs à comprendre les contraintes de votre code. Fondamentalement, les tests unitaires sont une forme de documentation.
Les tests unitaires vérifient qu'une seule unité de code renvoie des résultats corrects et que le comportement d'un programme ne change pas lorsque les détails sont modifiés.
L'utilisation de tests unitaires facilite la modularisation de vos codes de recherche. Cela peut être particulièrement important si vous commencez à essayer de cibler votre code sur une nouvelle plate-forme, par exemple si vous souhaitez le paralléliser ou l'exécuter sur un ordinateur GPGPU.
Surtout, les tests unitaires vous donnent l'assurance que les résultats de la recherche que vous produisez à l'aide de vos codes sont valides et vérifiables.
Je remarque que vous avez mentionné le test de régression dans votre question. Dans de nombreux cas, les tests de régression sont réalisés par l'exécution automatisée et régulière de tests unitaires et / ou de tests d'intégration (qui vérifient que les éléments de code fonctionnent correctement lorsqu'ils sont combinés; en informatique scientifique, cela se fait souvent en comparant la sortie à des données expérimentales ou à la méthode précédente). résultats de programmes antérieurs approuvés.) On dirait que vous utilisez déjà des tests d'intégration ou des tests unitaires au niveau de gros composants complexes.
Ce que je dirais, c'est qu'à mesure que les codes de recherche deviennent de plus en plus complexes et s'appuient sur le code et les bibliothèques d'autres personnes, il est important de comprendre où l'erreur se produit quand cela se produit. Le test unitaire permet à l'erreur d'être repérée beaucoup plus facilement.
Vous pouvez trouver la description, les preuves et les références à la section 7 «Planifier les erreurs» du document sur les meilleures pratiques pour l'informatique scienti fi que que j'ai coécrit : il introduit également le concept complémentaire de programmation défensive.
la source
Dans mes cours deal.II, j'enseigne que les logiciels qui ne comportent pas de tests ne fonctionnent pas correctement (et soulignons que j’ai dit à dessein « ne fonctionne pas correctement» et non « peut ne pas fonctionner correctement»).
Bien sûr, je vis selon le mantra - c'est la raison pour laquelle deal.II est venu d'exécuter 2 500 tests avec chaque commit ;-)
Plus sérieusement, je pense que Matt définit déjà bien les deux classes de tests. Nous écrivons des tests unitaires pour le matériel de niveau inférieur et cela progresse naturellement vers des tests de régression pour le matériel de niveau supérieur. Je ne pense pas que je pourrais tracer une limite claire qui séparerait nos tests d’un côté ou de l’autre, il y en a certainement beaucoup qui tracent la ligne où une personne a examiné le résultat et l’a trouvée largement raisonnable (test unitaire?) sans l'avoir examiné jusqu'à la dernière précision (test de régression?).
la source
Oui et non. N'oubliez certainement pas les routines fondamentales de l'ensemble d'outils de base que vous utilisez pour vous rendre la vie plus facile, telles que les routines de conversion, les mappages de chaînes, la physique et les mathématiques de base, etc. vous préférerez peut-être les tester en tant que tests fonctionnels plutôt qu'en tant qu'unités. En outre, soulignez et insistez beaucoup sur les classes et les entités dont le niveau et l'utilisation vont beaucoup changer (par exemple à des fins d'optimisation) ou dont les détails internes vont être modifiés pour une raison quelconque. L'exemple le plus typique est une classe enveloppant une énorme matrice mappée à partir d'un disque.
la source
Absolument!
Quoi, ça ne vous suffit pas?
Dans la programmation scientifique plus que tout autre type, nous développons en essayant de faire correspondre un système physique. Comment saurez-vous si vous avez fait cela autrement qu'en testant? Avant même de commencer à coder, décidez de la manière dont vous allez utiliser votre code et calculez quelques exemples d’exécution. Essayez d'attraper tous les cas possibles. Faites-le de manière modulaire - par exemple, pour un réseau de neurones, vous pouvez effectuer un ensemble de tests pour un seul neurone et un ensemble de tests pour un réseau de neurones complet. Ainsi, lorsque vous commencez à écrire du code, vous pouvez vous assurer que votre neurone fonctionne avant de commencer à travailler sur le réseau. Travailler dans des étapes comme celle-ci signifie que lorsque vous rencontrez un problème, vous ne devez tester que la «dernière» étape de code la plus récente, les étapes précédentes ont déjà été testées.
De plus, une fois que vous avez les tests, si vous devez réécrire le code dans une autre langue (conversion en CUDA, par exemple), ou même si vous ne faites que le mettre à jour, vous avez déjà les cas de test et vous pouvez les utiliser pour créer Assurez-vous que les deux versions de votre programme fonctionnent de la même manière.
la source
Oui.
L'idée qu'un code est écrit sans tests unitaires est un anathème. Sauf si vous prouvez que votre code est correct, puis que la preuve est correcte = P.
la source
J'aborderais cette question de manière pragmatique plutôt que dogmatique. Posez-vous la question: "Qu'est-ce qui pourrait mal tourner dans la fonction X?" Imaginez ce qui arrive à la sortie lorsque vous introduisez dans le code des bogues typiques: un préfacteur incorrect, un index erroné, ... et écrivez ensuite des tests unitaires susceptibles de détecter ce type de bogue. Si pour une fonction donnée il n'y a aucun moyen d'écrire de tels tests sans répéter le code de la fonction elle-même, alors ne le faites pas - mais pensez aux tests du niveau immédiatement supérieur.
Un problème beaucoup plus important avec les tests unitaires (ou en fait tous les tests) dans le code scientifique est de savoir comment traiter les incertitudes de l'arithmétique en virgule flottante. Autant que je sache, il n’existe pas encore de bonne solution générale.
la source
Je suis désolé pour Tangurena - ici, le mantra est "Un code non testé est un code cassé" et cela vient du patron. Plutôt que de répéter toutes les bonnes raisons de faire des tests unitaires, je souhaite simplement ajouter quelques détails.
la source
J'ai utilisé les tests unitaires à bon escient sur plusieurs codes à petite échelle (c.-à-d. Un seul programmeur), y compris la troisième version de mon code d'analyse de mémoire en physique des particules.
Les deux premières versions s’étaient effondrées sous leur propre poids et la multiplication des interconnexions.
D'autres ont écrit que l'interaction entre les modules est souvent le lieu où le codage scientifique est interrompu, et ils ont raison à ce sujet. Mais il est beaucoup plus facile à diagnostiquer les problèmes quand vous pouvez montrer de façon concluante que chaque module est en train de faire ce qu'il est censé faire.
la source
Une approche légèrement différente que j'ai utilisée lors du développement d'un solveur chimique (pour les domaines géologiques complexes) était ce que vous pourriez appeler les tests unitaires par copie et collage d'extraits .
Construire un harnais de test pour le code d'origine intégré dans un grand modélisateur de systèmes chimiques n'était pas réalisable dans le délai imparti.
Cependant, j'ai été en mesure de créer un ensemble d'extraits de plus en plus complexes montrant le fonctionnement de l'analyseur syntaxique (Boost Spirit) pour les formules chimiques, en tant que tests unitaires pour différentes expressions.
Le test unitaire final le plus complexe était très proche du code nécessaire dans le système, sans qu'il soit nécessaire de changer ce code pour qu'il soit simulatoire. J'ai donc pu copier mon code testé par unité.
Ce qui fait de cela plus qu'un simple exercice d'apprentissage et une véritable suite de régression, ce sont deux facteurs: les tests unitaires conservés dans la source principale et exécutés dans le cadre d'autres tests pour cette application (et oui, ils ont effectivement eu un effet secondaire de Boost. Esprit changeant 2 ans plus tard) - du fait que le code copié et collé était très peu modifié dans l'application réelle, il pouvait contenir des commentaires renvoyant aux tests unitaires pour les aider à rester synchronisés.
la source
Pour les bases de code plus volumineuses, les tests (pas nécessairement les tests unitaires) des éléments de haut niveau sont utiles. Les tests unitaires pour certains algorithmes plus simples sont également utiles pour vous assurer que votre code ne fait pas de bêtises parce que votre fonction d'assistance utilise à la
sin
place decos
.Mais pour le code de recherche global, il est très difficile d'écrire et de maintenir des tests. Les algorithmes ont tendance à être volumineux sans résultats intermédiaires significatifs pouvant comporter des tests évidents et prenant souvent beaucoup de temps à s'exécuter avant d'obtenir un résultat. Bien sûr, vous pouvez tester des références qui ont donné de bons résultats, mais ce n'est pas un bon test dans le sens du test unitaire.
Les résultats sont souvent des approximations de la vraie solution. Bien que vous puissiez tester vos fonctions simples si elles sont précises jusqu’à epsilon, il sera très difficile de vérifier si, par exemple, un résultat de maillage est correct ou non, ce qui avait déjà été évalué par inspection visuelle de l’utilisateur (vous).
Dans de tels cas, les tests automatisés ont souvent un rapport coût / bénéfice trop élevé. Je recommande quelque chose de mieux: écrire des programmes de test. Par exemple, j'ai écrit un script python de taille moyenne pour créer des données sur les résultats, telles que des histogrammes de tailles d'arête et d'angles d'un maillage, une zone du triangle le plus grand et le plus petit et leur rapport, etc.
Je peux à la fois l'utiliser pour évaluer les maillages d'entrée et de sortie pendant le fonctionnement normal et l' utiliser pour effectuer un contrôle de cohérence après avoir modifié l'algorithme. Lorsque je change d’algorithme, je ne sais pas toujours si le nouveau résultat est meilleur, car il n’existe souvent aucune mesure absolue de la meilleure approximation. Mais en générant de telles métriques, je peux dire à propos de certains facteurs ce qui est mieux comme "La nouvelle variante a finalement un meilleur rapport angulaire mais un taux de convergence plus mauvais".
la source