web-dev-qa-db-fra.com

intégration continue pour les logiciels scientifiques

Je ne suis pas ingénieur logiciel. Je suis doctorant dans le domaine des géosciences.

Il y a presque deux ans, j'ai commencé à programmer un logiciel scientifique. Je n'ai jamais utilisé l'intégration continue (CI), principalement parce qu'au début je ne savais pas qu'elle existait et j'étais la seule personne à travailler sur ce logiciel.

Maintenant que la base du logiciel fonctionne, d'autres personnes commencent à s'y intéresser et veulent contribuer au logiciel. Le plan est que d'autres personnes dans d'autres universités mettent en œuvre des ajouts au logiciel de base. (J'ai peur qu'ils puissent introduire des bugs). De plus, le logiciel est devenu assez complexe et est devenu de plus en plus difficile à tester et je prévois également de continuer à travailler dessus.

Pour ces deux raisons, je pense de plus en plus à l'utilisation de CI. Comme je n'ai jamais reçu de formation d'ingénieur logiciel et que personne autour de moi n'a jamais entendu parler de CI (nous sommes des scientifiques, pas de programmeurs), j'ai du mal à démarrer mon projet.

J'ai quelques questions où j'aimerais obtenir des conseils:

Tout d'abord une brève explication du fonctionnement du logiciel:

  • Le logiciel est contrôlé par un fichier .xml contenant tous les paramètres requis. Vous démarrez le logiciel en passant simplement le chemin d'accès au fichier .xml comme argument d'entrée et il s'exécute et crée quelques fichiers avec les résultats. Un seul passage peut prendre environ 30 secondes.

  • C'est un logiciel scientifique. Presque toutes les fonctions ont plusieurs paramètres d'entrée, dont les types sont pour la plupart des classes assez complexes. J'ai plusieurs fichiers .txt avec de gros catalogues qui sont utilisés pour créer des instances de ces classes.

Venons-en maintenant à mes questions:

  1. tests unitaires, tests d'intégration, tests de bout en bout? : Mon logiciel compte maintenant environ 30.000 lignes de code avec des centaines de fonctions et ~ 80 classes. Cela me semble un peu étrange de commencer à écrire des tests unitaires pour des centaines de fonctions qui sont déjà implémentées. J'ai donc pensé à créer simplement des cas de test. Préparez 10 à 20 fichiers .xml différents et laissez le logiciel s'exécuter. Je suppose que c'est ce qu'on appelle des tests de bout en bout? Je lis souvent que vous ne devriez pas faire cela, mais peut-être que c'est ok comme début si vous avez déjà un logiciel qui fonctionne? Ou est-ce simplement une idée stupide d'essayer d'ajouter CI à un logiciel déjà fonctionnel.

  2. Comment écrivez-vous les tests unitaires si les paramètres de la fonction sont difficiles à créer? supposons que j'ai une fonction double fun(vector<Class_A> a, vector<Class_B>) et généralement, je devez d'abord lire dans plusieurs fichiers texte pour créer des objets de type Class_A et Class_B. J'ai pensé à créer des fonctions factices comme Class_A create_dummy_object() sans lire les fichiers texte. J'ai aussi pensé à implémenter une sorte de sérialisation . (Je ne prévois pas de tester la création des objets de classe car ils ne dépendent que de plusieurs fichiers texte)

  3. Comment écrire des tests si les résultats sont très variables? Mon logiciel utilise de grandes simulations de monte-carlo et fonctionne de manière itérative. Habituellement, vous avez ~ 1000 itérations et à chaque itération, vous créez ~ 500-20 000 instances d'objets basées sur des simulations de monte-carlo. Si un seul résultat d'une itération est un peu différent, toutes les itérations à venir sont complètement différentes. Comment gérez-vous cette situation? Je suppose que c'est un gros point contre les tests de bout en bout, car le résultat final est très variable?

Tout autre conseil avec CI est très apprécié.

22
user7431005

Tester un logiciel scientifique est difficile, à la fois à cause du sujet complexe et à cause des processus de développement scientifique typiques (aka. Pirater jusqu'à ce qu'il fonctionne, ce qui n'aboutit généralement pas à une conception testable). C'est un peu ironique étant donné que la science doit être reproductible. Ce qui change par rapport au logiciel "normal" n'est pas de savoir si les tests sont utiles (oui!), Mais quels types de tests sont appropriés.

Gestion de l'aléatoire: toutes les exécutions de votre logiciel DOIVENT être reproductibles. Si vous utilisez des techniques de Monte Carlo, vous devez permettre de fournir une graine spécifique pour le générateur de nombres aléatoires.

  • Il est facile d'oublier cela, par ex. lors de l'utilisation de la fonction Rand() de C qui dépend de l'état global.
  • Idéalement, un générateur de nombres aléatoires est transmis en tant qu'objet explicite via vos fonctions. L'en-tête de bibliothèque standard random de C++ 11 facilite beaucoup les choses.
  • Au lieu de partager un état aléatoire entre les modules du logiciel, j'ai trouvé utile de créer un deuxième RNG qui est ensemencé par un nombre aléatoire à partir du premier RNG. Ensuite, si le nombre de demandes au RNG par l'autre module change, la séquence générée par le premier RNG reste la même.

Les tests d'intégration sont parfaitement bien. Ils sont bons pour vérifier que les différentes parties de votre logiciel fonctionnent correctement ensemble et pour exécuter des scénarios concrets.

  • Un niveau de qualité minimum "ça ne plante pas" peut déjà être un bon résultat de test.
  • Pour des résultats plus solides, vous devrez également vérifier les résultats par rapport à une base de référence. Cependant, ces contrôles devront être quelque peu tolérants, par ex. tenir compte des erreurs d'arrondi. Il peut également être utile de comparer des statistiques récapitulatives au lieu de lignes de données complètes.
  • Si la vérification par rapport à une ligne de base est trop fragile, vérifiez que les sorties sont valides et satisfont à certaines propriétés générales. Ceux-ci peuvent être généraux ("les emplacements sélectionnés doivent être distants d'au moins 2 km") ou spécifiques au scénario, par ex. "Un emplacement sélectionné doit se trouver dans cette zone".

Lors de l'exécution de tests d'intégration, il est judicieux d'écrire un exécuteur de test en tant que programme ou script distinct. Ce lanceur de test effectue la configuration nécessaire, exécute l'exécutable à tester, vérifie tous les résultats et nettoie ensuite.

Test unitaire les vérifications de style peuvent être assez difficiles à insérer dans un logiciel scientifique car le logiciel n'a pas été conçu pour cela. En particulier, les tests unitaires deviennent difficiles lorsque le système testé a de nombreuses dépendances/interactions externes. Si le logiciel n'est pas purement orienté objet, il n'est généralement pas possible de simuler/stub ces dépendances. J'ai trouvé préférable d'éviter largement les tests unitaires pour de tels logiciels, à l'exception des fonctions mathématiques pures et des fonctions utilitaires.

Même quelques tests valent mieux qu'aucun test. Combiné avec la vérification "il faut compiler", c'est déjà un bon début pour une intégration continue. Vous pouvez toujours revenir et ajouter plus de tests plus tard. Vous pouvez ensuite hiérarchiser les zones du code les plus susceptibles de se briser, par exemple parce qu'ils obtiennent plus d'activité de développement. Pour voir quelles parties de votre code ne sont pas couvertes par les tests unitaires, vous pouvez utiliser des outils de couverture de code.

Test manuel: Surtout pour les domaines à problèmes complexes, vous ne pourrez pas tout tester automatiquement. Par exemple. Je travaille actuellement sur un problème de recherche stochastique. Si je teste que mon logiciel produit toujours le même résultat , je ne peux pas l'améliorer sans casser les tests. Au lieu de cela, j'ai facilité les tests manuels : j'exécute le logiciel avec une valeur de départ fixe et j'obtiens un visualisation du résultat (selon vos préférences, R, Python/Pyplot et Matlab permettent tous d'obtenir facilement des visualisations de haute qualité de vos ensembles de données). Je peux utiliser cette visualisation pour vérifier que les choses ne se sont pas vraiment mal passées. De même, le suivi de la progression de votre logiciel via la sortie de journalisation peut être une technique de test manuel viable, du moins si je peux sélectionner le type d'événements à consigner.

23
amon

Cela me semble un peu étrange de commencer à écrire des tests unitaires pour des centaines de fonctions qui sont déjà implémentées.

Vous voudrez (généralement) écrire les tests lorsque vous modifiez lesdites fonctions. Vous n'avez pas besoin de vous asseoir et d'écrire des centaines de tests unitaires pour les fonctions existantes, ce serait (en grande partie) une perte de temps. Le logiciel fonctionne (probablement) correctement tel quel. Le but de ces tests est de s'assurer que les futurs changements ne cassent pas l'ancien comportement. Si vous ne changez plus jamais une fonction particulière, cela ne vaudra probablement jamais la peine de prendre le temps de la tester (car elle fonctionne actuellement, a toujours fonctionné et continuera probablement de fonctionner). Je recommande la lecture Travailler efficacement avec le code hérité par Michael Feathers sur ce front. Il a d'excellentes stratégies générales pour tester des choses qui existent déjà, y compris des techniques de rupture de dépendance, des tests de caractérisation (sortie de fonction copier/coller dans la suite de tests pour vous assurer de maintenir un comportement de régression), et bien plus encore.

Comment écrivez-vous des tests unitaires si les paramètres de fonction sont difficiles à créer?

Idéalement, vous ne le faites pas. Au lieu de cela, vous rendez les paramètres plus faciles à créer (et donc rendre votre conception plus facile à tester). Certes, les modifications de conception prennent du temps, et ces refactorings peuvent être difficiles sur les projets hérités comme le tien. TDD (Test Driven Development) peut vous y aider. Si les paramètres sont très difficiles à créer, vous aurez beaucoup de mal à écrire des tests dans un style test-first.

À court terme, utilisez des simulacres, mais méfiez-vous des moqueries de l'enfer et des problèmes qui les accompagnent à long terme. Comme j'ai grandi en tant qu'ingénieur logiciel, cependant, j'ai réalisé que les simulacres sont presque toujours une mini-odeur qui essaie de résumer un problème plus important et ne résout pas le problème principal. J'aime l'appeler "emballage de crotte", parce que si vous mettez un morceau de papier d'aluminium sur un peu de caca de chien sur votre tapis, ça pue toujours. Ce que vous avez à faire, c'est de vous lever, de ramasser le caca et de le jeter à la poubelle, puis de le retirer. C'est évidemment plus de travail, et vous risquez d'avoir des matières fécales sur vos mains, mais mieux pour vous et votre santé à long terme. Si vous continuez à emballer ces caca, vous ne voudrez pas vivre plus longtemps dans votre maison. Les simulacres sont de nature similaire.

Par exemple, si vous avez votre Class_A Difficile à instancier car vous devez lire 700 fichiers, alors vous pouvez simplement vous en moquer. La prochaine chose que vous savez, votre maquette devient obsolète, et le réel Class_A Fait quelque chose de très différent de la maquette , et vos tests sont toujours réussis même s'ils devraient échouer. Une meilleure solution consiste à décomposer Class_A en plus facile à utiliser/tester les composants, et tester ces composants à la place. Peut-être écrire un test d'intégration qui frappe réellement le disque et s'assurer que Class_A Fonctionne dans son ensemble. Ou peut-être simplement avoir un constructeur pour Class_A Que vous pouvez instancier avec une simple chaîne (représentant vos données) au lieu d'avoir à lire à partir du disque.

Comment écrire des tests si les résultats sont très variables?

Quelques conseils:

1) Utilisez des inverses (ou plus généralement, des tests basés sur les propriétés). Quel est le fft de [1,2,3,4,5]? Aucune idée. Qu'est-ce que ifft(fft([1,2,3,4,5]))? Doit être [1,2,3,4,5] (Ou proche de lui, des erreurs en virgule flottante peuvent apparaître).

2) Utilisez des assertions "connues". Si vous écrivez une fonction déterminante, il peut être difficile de dire quel est le déterminant d'une matrice 100x100. Mais vous savez que le déterminant de la matrice d'identité est 1, même si c'est 100x100. Vous savez également que la fonction doit retourner 0 sur une matrice non inversible (comme un 100x100 plein de tous les 0).

) Utilisez des assertions approximatives au lieu des assertions exactes . J'ai écrit il y a quelque temps du code qui enregistrait deux images en générant des liens des points qui créent une correspondance entre les images et effectuent une déformation entre elles pour les faire correspondre. Il pourrait s'inscrire à un niveau sous-pixel. Comment pouvez-vous le tester? Des choses comme:

EXPECT_TRUE(reg(img1, img2).size() < min(img1.size(), img2.size()))

puisque vous ne pouvez vous inscrire que sur des parties qui se chevauchent, l'image enregistrée doit être plus petite ou égale à votre plus petite image), et aussi:

scale = 255
EXPECT_PIXEL_EQ_WITH_TOLERANCE(reg(img, img), img, .05*scale)

car une image enregistrée sur elle-même doit être proche de elle-même, mais vous pouvez rencontrer un peu plus que des erreurs en virgule flottante en raison de l'algorithme à portée de main, alors vérifiez simplement que chaque pixel est à +/- 5% de la plage valide (0-255 est une gamme commune, niveaux de gris). Doit au moins être de la même taille. Vous pouvez même juste test de fumée (c'est-à-dire l'appeler et vous assurer qu'il ne plante pas). En général, cette technique est meilleure pour les tests plus volumineux où le résultat final ne peut pas être (facilement) calculé a priori avant d'exécuter le test.

4) Utilisez OR STORE une graine de nombre aléatoire pour votre RNG.

Les exécutions doivent être reproductibles. Il est faux, cependant, que la seule façon d'obtenir une exécution reproductible est de fournir une graine spécifique à un générateur de nombres aléatoires. Parfois, les tests d'aléatoire sont précieux . J'ai vu des bogues dans le code scientifique qui surviennent dans des cas dégénérés générés de manière aléatoire . Au lieu de toujours appeler votre fonction avec la même graine, générez un aléatoire puis utilisez cette graine et enregistrez la valeur de la graine. De cette façon, chaque exécution a une graine aléatoire différente , mais si vous obtenez un plantage, vous pouvez réexécuter le résultat en utilisant la graine que vous avez enregistrée à déboguer. J'ai effectivement utilisé cela dans la pratique et cela a corrigé un bug, alors j'ai pensé que je le mentionnerais. Inconvénient: vous devez enregistrer vos exécutions de test. Côté supérieur: correction et correction des bogues.

HTH.

7
Matt Messersmith
  1. Types de test

    • Cela me semble un peu étrange de commencer à écrire des tests unitaires pour des centaines de fonctions qui sont déjà implémentées

      Pensez-y à l'envers: si un patch touchant plusieurs fonctions rompt l'un de vos tests de bout en bout, comment allez-vous déterminer lequel est le problème?

      C'est beaucoup plus facile d'écrire des tests unitaires pour des fonctions individuelles que pour l'ensemble du programme. C'est beaucoup plus facile pour être sûr d'avoir une bonne couverture d'une fonction individuelle. Il est beaucoup plus facile de refactoriser une fonction lorsque vous êtes sûr que les tests unitaires détecteront tous les cas d'angle que vous avez cassés.

      L'écriture de tests unitaires pour des fonctions déjà existantes est parfaitement normale pour quiconque a travaillé sur une base de code héritée. Ils sont un bon moyen de confirmer votre compréhension des fonctions en premier lieu et, une fois écrits, ils sont un bon moyen de trouver des changements de comportement inattendus.

    • Les tests de bout en bout valent également la peine. S'ils sont plus faciles à écrire, faites-les d'abord et ajoutez des tests unitaires ad hoc pour couvrir les fonctions qui vous inquiètent le plus pour les autres. Vous n'êtes pas obligé de tout faire en même temps.

    • Oui, l'ajout de CI au logiciel existant est raisonnable et normal.

  2. Comment écrire des tests unitaires

    Si vos objets sont vraiment chers et/ou complexes, écrivez des simulacres. Vous pouvez simplement lier les tests à l'aide de simulations séparément des tests à l'aide d'objets réels, au lieu d'utiliser le polymorphisme.

    Vous devriez de toute façon avoir un moyen facile de créer des instances - une fonction pour créer des instances factices est courante - mais avoir des tests pour le processus de création réel est également judicieux.

  3. Résultats variables

    Vous devez avoir certains invariants pour le résultat. Testez-les plutôt qu'une seule valeur numérique.

    Vous pouvez fournir un faux générateur de nombres pseudo-aléatoires si votre code de monte-carlo l'accepte comme paramètre, ce qui rendrait les résultats prévisibles au moins pour un algorithme bien connu, mais il est fragile à moins qu'il ne retourne littéralement le même nombre à chaque fois.

2
Useless

Dans une réponse avant amon déjà mentionné quelques points très importants. Permettez-moi d'en ajouter un peu plus:

1. Différences entre le développement de logiciels scientifiques et les logiciels commerciaux

Pour les logiciels scientifiques, l'accent est normalement mis sur le problème scientifique, bien sûr. Les problèmes consistent davantage à gérer le contexte théorique, à trouver la meilleure méthode numérique, etc. Le logiciel n'est qu'une, plus ou moins, petite partie du travail.

Le logiciel est dans la plupart des cas écrit par une ou seulement quelques personnes. Il est souvent écrit pour un projet spécifique. Lorsque le projet est terminé et que tout est publié, dans de nombreux cas, le logiciel n'est plus nécessaire.

Les logiciels commerciaux sont généralement développés par de grandes équipes sur une période plus longue. Cela nécessite beaucoup de planification pour l'architecture, la conception, les tests unitaires, les tests d'intégration, etc. Cette planification nécessite beaucoup de temps et d'expérience. Dans un environnement scientifique, il n'y a normalement pas de temps pour cela.

Si vous souhaitez convertir votre projet en un logiciel similaire à un logiciel commercial, vous devez vérifier les points suivants:

  • Avez-vous le temps et les ressources?
  • Quelle est la perspective à long terme du logiciel? Que se passera-t-il avec le logiciel lorsque vous aurez terminé votre travail et quitterez l'université?

2. Tests de bout en bout

Si le logiciel devient de plus en plus complexe et que plusieurs personnes y travaillent, des tests sont obligatoires. Mais comme amon déjà mentionné, l'ajout de tests unitaires aux logiciels scientifiques est assez difficile. Vous devez donc utiliser une approche différente.

Comme votre logiciel obtient son entrée à partir d'un fichier, comme la plupart des logiciels scientifiques, il est parfait pour créer plusieurs exemples de fichiers d'entrée et de sortie. Vous devez exécuter ces tests automatiquement sur chaque version et comparer les résultats avec vos échantillons. Cela pourrait être un très bon remplacement pour les tests unitaires. Vous obtenez également des tests d'intégration de cette façon.

Bien sûr, pour obtenir des résultats reproductibles, vous devez utiliser la même graine pour votre générateur de nombres aléatoires, comme amon l'a déjà écrit.

Les exemples doivent couvrir les résultats typiques de votre logiciel. Cela devrait également inclure les cas Edge de votre espace de paramètres et des algorithmes numériques.

Vous devriez essayer de trouver des exemples qui ne nécessitent pas trop de temps pour s'exécuter, mais qui couvrent toujours les cas de test typiques.

3. Intégration continue

Étant donné que l'exécution des exemples de test peut prendre un certain temps, je pense qu'une intégration continue n'est pas possible. Vous devrez probablement discuter des parties supplémentaires avec vos collègues. Par exemple, ils doivent correspondre aux méthodes numériques utilisées.

Je pense donc qu'il vaut mieux faire l'intégration d'une manière bien définie après avoir discuté du contexte théorique et des méthodes numériques, des tests minutieux, etc.

Je ne pense pas que ce soit une bonne idée d'avoir une sorte d'automatisme pour une intégration continue.

Au fait, utilisez-vous un système de contrôle de version?

4. Test de vos algorithmes numériques

Si vous comparez des résultats numériques, par exemple lors de la vérification de vos sorties de test, vous ne devez pas vérifier l'égalité des nombres flottants. Il peut toujours y avoir des erreurs d'arrondi. Vérifiez plutôt si la différence est inférieure à un seuil spécifique.

C'est également une bonne idée de comparer vos algorithmes avec différents algorithmes ou de formuler le problème scientifique d'une manière différente et de comparer les résultats. Si vous obtenez les mêmes résultats en utilisant deux méthodes indépendantes ou plus, c'est une bonne indication que votre théorie et votre implémentation sont correctes.

Vous pouvez faire ces tests dans votre code de test et utiliser l'algorithme le plus rapide pour votre code de production.

1
bernie
  1. Ce n'est jamais une idée stupide d'ajouter CI. Par expérience, je sais que c'est la voie à suivre lorsque vous avez un projet open source où les gens sont libres de contribuer. CI vous permet d'empêcher les gens d'ajouter ou de modifier du code si le code rompt votre programme, il est donc presque inestimable d'avoir une base de code fonctionnelle.

    Lorsque vous envisagez des tests, vous pouvez certainement fournir des tests de bout en bout (je pense que c'est une sous-catégorie de tests d'intégration) pour vous assurer que votre flux de code fonctionne comme il se doit. Vous devez fournir au moins quelques tests unitaires de base pour vous assurer que les fonctions produisent les bonnes valeurs, dans le cadre des tests d'intégration peuvent compenser les autres erreurs commises pendant le test.

  2. La création d'objets de test est quelque chose d'assez difficile et laborieux. Vous avez raison de vouloir fabriquer des objets factices. Ces objets devraient avoir une valeur par défaut, mais la casse Edge, pour laquelle vous savez certainement quelle devrait être la sortie.

  3. Le problème avec les livres sur ce sujet est que le paysage de CI (et d'autres parties de devops) évolue si rapidement que tout dans un livre sera probablement obsolète quelques mois plus tard. Je ne connais aucun livre qui pourrait vous aider, mais Google devrait, comme toujours, être votre sauveur.

  4. Vous devez exécuter vos tests vous-même plusieurs fois et effectuer une analyse statistique. De cette façon, vous pouvez implémenter certains cas de test où vous prenez la médiane/moyenne de plusieurs exécutions et la comparez à votre analyse, pour savoir quelles valeurs sont correctes.

Quelques conseils:

  • Utilisez l'intégration des outils CI dans votre plateforme GIT pour empêcher le code cassé d'entrer dans votre base de code.
  • arrêtez la fusion du code avant que l'examen par les pairs n'ait été effectué par d'autres développeurs. Cela rend les erreurs plus facilement connues et empêche à nouveau le code cassé d'entrer dans votre base de code.
1
Pelican

Mon conseil serait de choisir soigneusement la façon dont vous déployez vos efforts. Dans mon domaine (bioinformatique), les algorithmes de pointe évoluent si rapidement qu'il est préférable de dépenser de l'énergie pour la vérification des erreurs de votre code sur l'algorithme lui-même.

Cela dit, ce qui est apprécié, c'est:

  • est-ce la meilleure méthode à l'époque, en terme d'algorithme?
  • la facilité de portage vers différentes plates-formes de calcul (différents environnements HPC, versions de système d'exploitation, etc.)
  • robustesse - fonctionne-t-il sur MON jeu de données?

Votre instinct pour construire une base de code à l'épreuve des balles est noble, mais il convient de se rappeler que ce n'est pas un produit commercial. Rendez-le aussi portable que possible, à l'épreuve des erreurs (pour votre type d'utilisateur), pratique pour que d'autres contribuent, puis concentrez-vous sur l'algorithme lui-même

0
pufferfish