Info sur JMRI:
Outils de Développement
Structure
Techniques et Standards
Comment Faire
Infos Fonctionnelles
Contexte Infos

JMRI: Les tests unitaires avec JUnit

Introduction

JJUnit est un système de construction de "tests unitaires" du logiciel. Les tests unitaires sont de petits tests qui vous assurent que les parties individuelles du logiciel font ce qu'elles sont censées faire. Dans un projet distribué comme JMRI, où il y a beaucoup de développeurs qui peuvent perdre la communication avec les autres, les tests unitaires sont une bonne façon pour s'assurer que le code n'a pas été cassé par un changement.

Pour plus d'informations sur JUnit, voir la page d'accueil JUnit. Un exemple très intéressant du développement basé sur les tests est disponible à partir du livre de Robert Martin .

Certaines classes ont des tests JUnit disponibles. C'est bon d'ajouter des tests JUnit quand vous apportez des modifications, tester vos nouvelles fonctionnalités pour s'assurer qu'elle est au travail, et continue à travailler), lorsque vous devez comprendre le code que quelqu'un a fait (les tests documentent exactement ce qui doit arriver!), et quand vous traquer un bug (assurez-vous qu'il ne revient pas).

Exécutez les Tests

Pour exécuter les tests existants, dire ant antalltest Cela compilera le code de test, qui est dans le sous-répertoire "test" du répertoire "java" dans nos distributions habituelles de code, et puis exécutez les tests sous une interface graphique. (Pour vous assurer que vous avez tout recompilé, vous voudrez peut-être faire ant clean en premier). Si vous connaissez le nom de votre classe de test, ou la classe de test pour votre paquet, vous pouvez exécuter directement avec le script "runTest" : ant tests
./runtest.cshjmri.jmrit.powerpanel.PowerPanelTest
Le première ligne compile tout le code de test, et la seconde effectue un test spécifique ou une suite de test.

Exécution de test d'Intégration Continue

L'environnement d'intégration continue détecte des changements dans le répertoire de code, reconstruit le code, effectue une variété de contrôles. Si aucun problème fatal n'est trouvé, le processus d'intégration continue exécute la cible ant "alltest" contre la construction pour exécuter les tests contre la construction réussie du code de base.

Rapport d'Erreur

Si un test échoue durant l'exécution de l'intégration continue de "alltest", un e-mail est envoyé à la liste e-mail jmri-build ainsi qu'aux développeurs qui ont vérifié le code qui a été inclus dans la construction.

Vous pouvez visiter le site web pour vous inscrire sur la liste e-mail de jmri-buildpour obtenir les mauvaises nouvelles aussi rapidement que possible, ou surveiller pour voir les archives de la liste e-mail et voir les journaux du passé. Ou vous pouvez surveiller le "tableau de bord" sur le site web intégration continue.

( Quand la construction réussit, rien n'est envoyé, pour pour réduire le trafic )

Rapports de couverture de code

Comme élément d'exécution des tests, Jenkins accumule les informations sur la part de code exécuté, appelée "couverture de code". Nous utilisons l' outil JaCoCo pour faire le décompte. Il fournit des rapports détaillés à des niveaux multiples:

Écriture de Tests

Par convention, nous avons un "test" d'observation de classe de ( presque ) toutes les classes réelles. Le répertoire de "test" contient un arbre de paquet répertoires parallèles à l'arbre source. Chaque classe de test a le même nom que la classe devant être testée, excepté avec "Test" annexé, et qui apparaît dans le "test" de l'arbre source. Par exemple, le code source de la classe "jmri.Version" est dans "src/jmri/Version.java", et sa classe de test est "jmri.VersionTest" trouvé dans "test//jmri/VersionTest.java".

Il y a des classes supplémentaires qui sont utilisées pour grouper des classes de test pour un paquet particulier dans des suites de test JUnit.

Écriture de Test Supplémentaires pour une Classe Existante

Pour écrire des tests supplémentaires pour une classe ayant des tests existants, premièrement localisez la classe de test. ( Si elle n'existe pas, voir la section ci-dessous au sujet de l'écriture de tests pour une nouvelle classe )

Pour cette classe test, ajoutez une ou plusieurs méthodes de test utilisant les conventions JUnit. Basiquement, chaque méthode nécessite un nom qui démarre avec "test", exemple: "testFirst" et doit avoir une signature "publique vide". JUnit gèrera tout ce qui suit. En général, les méthodes de test doivent être petites, testant juste une partie de l'opération classe. C'est pourquoi elles sont appelées tests "unitaire".

Écriture de Tests pour une Nouvelle Classe

( Nécessite des infos ici: basiquement, copier quelques autres paquet, et ne pas oublier de mettre une entrée dans l'enceinte du paquet de suite de test )

Clé de Test Métaphores

Manipulation de la Sortie Log4J Pour les Tests

JMRI utilise Log4j pour gérer l'enregistrement des différentes conditions incluant messages erreurs et information de débogage. Les tests sont destinés à fonctionner sans sortie d'erreur ou d'avertissement, de sorte qu'il est immédiatement apparent qu'un rapport standard vide indique qui ils ont été parcouru proprement.

L'usage de Log4j dans les classes de test elle-mêmes a deux aspects:

  1. Il est parfaitement Ok pour utiliser les états Log.debug(...) pour le rendre facile pour les problèmes de débogage dans les tests d'états. log.info(...) peut être utilisé avec parcimonie pour indiquer la progression normale, parce qu'il est normalement désactivé pendant l'exécution des tests.
  2. En général, log.warn ou log.error doit seulement être utilisé quand le test va ensuite déclencher une assertion ou une exception de JUnit, parce que le fait qu'une erreur est enregistrée, elle ne se montre pas directement dans le résumé des résultats JUnit.
D'autre part, vous pourriez vouloir provoquer délibérément des erreurs dans le code devant être testé pour être sûr que les conditions sont gérées proprement. Ceci produira souvent des messages log.error(...) ou log.warn(...); qui doivent être interceptés et vérifiés.

Pour permettre ceci, JMRI fonctionne et il utilise des tests avec un dispositif log4j spécial, qui stocke les messages de sorte que les tests JUnit peuvent les regarder avant de les transmettre dans le journal. Il y a deux aspects pour faire ce travail:

  1. Toutes les classes d'essai doivent inclure le code commun dans leur configuration() et leur code de désassemblage() pour être sûr que log4j est proprement initié, et que le dispositif personnalisé discute quand un test démarre ou s'arrête. @Before
    public void setUp() throws Exception {
    jmri.util.JUnitUtil.setUp();
    }
    @After
    public void tearDown() throws Exception {
    jmri.util.JUnitUtil.tearDown();
    }
  2. Quand un test invoque délibérément un message, il faut alors utiliser le contrôle pour voir si le message a été créé. Par exemple, si la classe en test devrait faire
    log.warn("Provoked message"); la case du test appelant devrait suivre avec la ligne:
    jmri.util.JUnitAppender.assertWarnMessage("Provoked message");
    Ce sera une erreur de JUnit si un message log.warn(...) ou log.error(...) est produit et qu'il ne correspond pas à un appel JUnitAppender.assertWarnMessage(...).
Dans tous les cas, l'ensemble de vos routines principales() devrait commencer par jmri.util.JUnitUtil.setUp(); de sorte qu'elles peuvent être exécutées de façon indépendante.

Note: Nos CI test exécutables sont configurés pour échouer si des messages FATAL ou ERROR sont émis au lieu d'être traités. Cela signifie que même si vous pouvez exécuter vos tests avec succès sur votre ordinateur, s'ils emettent des messages d'erreurs, mais vous ne serez pas en mesure de fusionner votre code dans le répertoire commun jusqu'à ce que ceux-ci soient traités.

Réinitialisation de l'InstanceManager

si vous testez du code qui va référencer l'InstanceManager, vous devez effacer et réinitialiser pour vous assurer d'obtenir des résultats reproductibles.

Dépendant du code utilisé par vos gestionnaires, votre mise en œuvre de configuration() devrait commencer par:

jmri.util.JUnitUtil.setUp();
jmri.util.JUnitUtil.resetInstanceManager();
jmri.util.JUnitUtil.initInternalTurnoutManager();
jmri.util.JUnitUtil.initInternalLightManager();
jmri.util.JUnitUtil.initInternalSensorManager();
( Vous pouvez omettre l'initialisation des gestionnaires dont vous n'avez pas besoin ) Voir la classe jmri.util.JUnitUtil pour la liste complète de ceux qui sont disponibles, et svp ajoutez en plus si vous avez besoin d'un que vous n'avez pas déjà.

Votre désassemblage() doit se terminer par

jmri.util.JUnitUtil.resetInstanceManager();
jmri.util.JUnitUtil.tearDown();

Travailler avec les Auditeurs

JMRI est une application multi tâches. Les Auditeurs pour les les objets JMRI sont informés sur les différentes tâches. Parfois, il faut attendre que cela se déroulent

Si vous voulez attendre pour une condition spécifique pour être vrai, ex: recevoir une réponse d'un objet, vous pouvez utiliser un appel à la méthode waitFor qui ressemble à:


    JUnitUtil.waitFor(()->{reply!=null}, "reply didn't arrive");
Le premier argument est une une fermeture lambda, un petit morceau de code qui est évaluer répétitivement jusqu'à être vrai. La chaîne, deuxième argument, est le texte de l'assertion ( message d'erreur ) que vous obtiendrez si la condition ne devient pas vrai dans un temps raisonnable.

L'attente d'un résultat spécifique est plus rapide et plus fiable. Si vous ne pouvez pas faire cela pour une raison quelconque, vous pouvez faire une attente basée sur un temps court


    JUnitUtil.releaseThread(this);
Celui-ci utilise un retard nominal.

Notez que celui-ci ne devrait pas être utilisé en synchronisme avec les tâches Swing Voir le Test Code Swing, une section faite pour ça.

En général,vous ne devriez pas avoir d'appels pour dormir(), attendre(), produire() dans votre code. Utilisez l'aide de JUnitUtil pour ceux en place.

Travailler avec les Tâches

( Voir la Section suivante pour voir comment travailler avec les Objets Swing (GUI*) et le Lien Swing/AWT)

Certains tests devront commencer les tâches, par exemple pour tester pour tester les commandes de signaux ou les aspects sur le réseau I/O

Principes généraux: vos tests doivent obéir à un fonctionnement fiable:

Par exemple, si la création d'une tâche est basée sur AbstractAutomat vous pouvez vérifier le démarrage avec:


    AbsractAutomat p = new MyThreadClass();
p.start();
JUnitUtil.waitFor(()->{return p.isRunning();}, "logic running");
et s'assurer de la fin avec:

    p.stop();
JUnitUtil.waitFor(()->{return !p.isRunning();}, "logic stopped");

Essai E/S

Certains environnements de test n'alignent pas automatiquement les opérations d'E/S tels que le flux au cours des essais. Si vous testez quelque chose qui fait 'E/S, par exemple un TrafficController, vous devrez ajouter une instruction "flush ()" sur tous vos flux de sortie. ( Avoir à attendre longtemps pour faire un test fiable est un indice de ce qui se passe quelque chose quelque part dans votre code )

Création de Fichier Temporaire dans les Essais

Testcases qui créent des fichiers temporaires doivent être soigneusement créées de sorte qu'il n'y aura pas de problèmes avec le chemin du fichier,la sécurité du système de fichiers, pré-existence du fichier, etc. Ces tests doivent également être rédigés d'une manière qui va opérer avec succès dans l'environnement construction d'intégration continue. Et les fichiers temporaire ne doivent pas devenir un élément du répertoire de code JMRI

Voici quelques idées qui peuvent aider à éviter ces types de problèmes.

Les questions ci-dessus ont été identifiées par l'intermédiaire d'une testcase qui a exécutée correctement sur un PC sous Windows pour les deux cibles ant "alltest" et "headlesstest", peu importe combien de fois il a été exécuté. dans l'environnement intégration continue le test a couru correctement la première fois après avoir été vérifié, mais a échoué pour chaque exécution de l'environnement d'intégration continue ultérieur de "headlesstest".Une fois que le test a été modifié sur la base des recommandations de fichiers temporaires présentées ici, le test est devenu stable sur plusieurs exécutions d'intégration continue de "headlesstest".

Essai du Code

AWT et le code Swing fonctionne sur une tâche séparée de tests JUnit. Une fois qu'un objet Swing ou AWT a été affiché (via show () ou setVisible (true)), il ne peut pas être accessible de manière fiable à partir de la tâche JUnit. Même l'utilisation de la technique de retard d'écouteur décrit ci-dessus n'est pas fiables.

Parce que nous utilisons les tests en mode "headless" pendant les constructions en intégration continue il est important que l'accès de Swing ( et AWT ) à des tests soit enfermé dans un mode de vérification:

if (!System.getProperty("jmri.headlesstest","false").equals("true")) {
suite.addTest(myTest.suite());
}

Ceci exécute Le myTest suite de test seulement quand un affichage est disponible.

Les test GUI* doivent fermer les fenêtres quand ils sont terminés, et en général se nettoient après eux-mêmes. Si vous voulez garder les fenêtres autour de sorte que vous pouvez les manipuler, par exemple pour le test manuel ou débogage vous pouvez utiliser le paramètre système jmri.demo pour contrôler que:

if (!System.getProperty("jmri.demo", "false").equals("false")) {
myFrame.setVisible(false);
myFrame.dispose();
}

Pour de nombreux tests, vous pourrez à la fois faire des tests fiables et améliorer la structure de votre code en séparant le code GUI (Swing) de la logique JMRI et des communications. Cela vous permet de vérifier le code logique séparément, mais en invoquant ces méthodes et de vérifier l'état de leurs mise à jour.

Essai code GUI Compliqué

Pour des tests GUI* plus compliqués, nous utilisons JFCUnit pour contrôler les interactions avec les objets Swing.

Pour un très simple exemple de l'utilisation de JFCUnit, voir le fichier test/jmri/util/SwingTestCaseTest.java.

Pour utiliser JFCUnit,vous héritez premièrement de votre classe depuis SwingTestCase au lieu de TestCase. Cela est suffisant pour obtenir le fonctionnement de base des tests Swing; la classe de base interrompt la tâche de test jusqu'à ce que Swing (en fait, le mécanisme d'événement AWT) a terminé tous les traitements après chaque appel Swing dans le test. Pour cette raison, les tests vont s'exécuter beaucoup plus lentement si vous vous déplacez par exemple le curseur de la souris pendant qu'ils s'exécutent)

Pour les tests plus complexe de l'interface graphique, vous pouvez appeler les différents aspects de l'interface et vérifier l'état interne en utilisant le code de test.

Essai Code Script

JMRI est fourni avec des exemples de scripts. Cette section discute de comment vous pouvez écrire des tests simples pour vous assurez qu'ils continuent de travailler.

Essai d'exemple de scripts Jython

Les tests de scripts peuvent être placés dans jython et appelés par java/test/jmri/jmrit/jython/SampleScriptTest.java.

Voir jmri_bindings_test .py exemple pour la syntaxe.

La classe SampleScriptTest est un espace réservé, et doit (éventuellement) être étendu pour ramasser automatiquement les fichiers, pour soutenir les essais Unit, etc.

Problèmes

JUnit utilise un classloader personnalisé, ce qui peut causer des problèmes pour trouver des singletons (classes uniques) et démarrer Swing. Si vous obtenez l'erreur de ne pas être en mesure de trouver ou de charger une classe, soupçonnez que l'ajout de la classe manquante au fichier test/junit/runner/ excluded.properties la corrigerait.

Comme un test seulement , vous pouvez essayer de régler l'option "-noloading" dans le main de n'importe quel test de classe avec lequelque vous avez des problèmes avec: static public void main(String[] args) {
String[] testCaseName = {"-noloading", LogixTableActionTest.class.getName()};
junit.swingui.TestRunner.main(testCaseName);
}

SVP ne laissez pas "-noloading" en place, car il empêche les gens de réexécuter le test dynamique. Au lieu de cela, le bon correctif à long terme est d'avoir toutes les classes ayant des problèmes chargeur JUnit incluses dans le fichier test/junit/runner/excluded.properties JUnit utilise ces propriétés pour décider comment gérer le chargement et le rechargement des classes.

GUI* = Interface Graphique Utilisateur