Java >> Tutoriel Java >  >> Tag >> JUnit

Test JUnit en Java

Bienvenue dans un autre tutoriel Java. La dernière fois, nous avons appris le flux de contrôle et revisité les bases de la logique. Nous allons maintenant apprendre à tester notre code à l'aide d'un framework appelé test JUnit.

Débogage

Au fur et à mesure que la logique se complique, nous allons commencer à remarquer davantage d'erreurs de programme appelées bugs . En fait, nous l'avons déjà remarqué dans la leçon précédente en jouant avec les instructions if.

Il s'avère que le processus de correction des erreurs est connu sous le nom de débogage. , et c'est une compétence extrêmement importante à avoir. Heureusement, Java contient de nombreux outils pour nous aider à identifier et à corriger les bogues. Mais avant d'en arriver là, essayons de regarder quelques exemples de bogues.

Exemple de mauvaise branche

Rappelez-vous précédemment quand nous avons introduit les instructions if ? Dans cette leçon, nous avons parlé d'un sujet connu sous le nom de création de branches. Le branchement augmente la complexité d'un programme en augmentant les chemins qu'un programme peut emprunter. À mesure que les branches augmentent, la possibilité que des bogues se développent augmente.

Maintenant, le défi est de s'assurer que toutes ces branches sont exemptes de bogues. Cependant, si une mauvaise branche ne s'exécute qu'une seule fois sur un million, cela peut prendre un certain temps avant que nous ne le remarquions. Cela suppose que nous ne vérifions pas déjà ce cas.

Si nous ne vérifions pas nos succursales à l'avance, nous rencontrerons inévitablement des problèmes plus tard. C'est là qu'intervient le débogage. Le débogage est le processus de recherche d'un bogue et de sa résolution. Commençons par un exemple.

public static boolean isPositive(int num) {
    // Assume false
    boolean state = false;
 
    if (num > 0) 
        System.out.println("num is positive");
        state = true;

    return state;
}

L'exemple ci-dessus est assez innocent. Pour le tester, nous allons essayer de le compiler et de l'exécuter à l'aide du volet d'interactions de Dr. Java.

Ici, nous supposons isPositive() est à l'intérieur d'une classe. Appelons-le MyMathWorkshop . De cette façon, nous pouvons facilement passer des valeurs à cette méthode en appelant quelque chose comme MyMathWorkshop.isPositive(num) . Tant que nous lui transmettons des nombres positifs, nous sommes satisfaits.

Cependant, nous finirons par tomber sur le cas où nous transmettrons un nombre négatif en entrée, et la méthode renverra true :

MyMathWorkshop.isPositive(2);    // Correctly returns true
MyMathWorkshop.isPositive(-7);   // Incorrectly returns true

Débogage avec une instruction d'impression

Alors que se passe-t-il? Heureusement, nous avons cette instruction d'impression que nous pouvons commencer à utiliser comme débogueur rudimentaire.

Si nous essayons un nombre positif, nous obtenons la valeur de retour correcte et l'instruction d'impression. Si nous essayons un nombre négatif, nous obtenons une valeur de retour incorrecte et aucune instruction d'impression. Cela nous indique que notre instruction if fonctionne car elle ne déclenche l'impression que lorsque l'entrée est positive.

Très bien, mais nous n'obtenons toujours pas la valeur de retour correcte pour les nombres négatifs. Alors, que faire on sait ?

Eh bien, nous savons que d'une manière ou d'une autre, le state la variable est écrasée quelle que soit l'entrée. Peut-être est-il possible que la ligne où nous définissons state à true n'est pas réellement groupé avec l'instruction if.

Essayons d'envelopper l'instruction if entre parenthèses pour garantir le state l'affectation n'est exécutée que pendant la branche d'entrée positive :

public static boolean isPositive(int num) {
    // Assume false
    boolean state = false;

    if (num > 0) {
        System.out.println("num is positive");
        state = true;
    }

    return state;
}

Ah ! Nous y voilà. Si nous essayons de passer une valeur négative, nous n'entrerons jamais dans le bloc if. En conséquence, le state ne seront jamais réaffectés et nous obtiendrons notre valeur de retour appropriée.

Leçons apprises

Alors, quelles sont les leçons apprises ici ? Premièrement, les relevés imprimés sont nos amis. Nous pouvons les exploiter pour isoler les zones du code où les problèmes peuvent être détectés. De plus, ils sont rapides et sales. Ils nous permettent de vérifier rapidement l'état des variables et d'autres objets sans nécessiter trop de code supplémentaire.

Bien sûr, nous ne devrions probablement pas mettre des déclarations imprimées partout. Ils peuvent rapidement obstruer le code et nuire à la lisibilité.

Et pendant que nous parlons de lisibilité, le premier extrait de code est un excellent exemple de style médiocre. Cependant, je recevrai probablement des commentaires désagréables pour celui-là.

À mon avis, nous devrions toujours utiliser des accolades sur un bloc de code, même s'il est trivial. Finalement, nous en prendrons l'habitude et nous ne regarderons jamais en arrière ! En fait, de nombreux IDE nous permettent de le faire par défaut, nous ne rencontrons donc jamais ce genre de problèmes.

Conception par contrat

Très bien, nous avons officiellement couvert le débogage ! Nous ne sommes pas allés dans les outils de débogage pour des raisons de portée, mais nous avons définitivement abordé l'idée principale.

Maintenant, passons aux tests. En particulier, couvrons les tests unitaires qui est un type spécial de test qui vérifie la fonctionnalité d'une "unité" de code. Une unité est un petit morceau de code qui peut être isolé et testé indépendamment.

Dans la plupart des cas, une « unité » est une méthode. Mais comment savoir quoi vérifier dans une unité de code ? C'est là qu'intervient la conception par contrat.

Exemple

Conception par contrat (DbC) est une méthodologie de programmation qui spécifie des règles pour faire des assertions. En particulier, DbC spécifie la précondition et la postcondition pour les opérations telles que les méthodes. Ces deux ensembles de règles spécifient le contrat qui doit être respecté par la méthode.

Pour comprendre DbC, prenons un exemple :

/**
 * Returns factorial of a number.
 * 
 * Precondition: 0 <= num <= 12
 * Postcondition: return == num!
 */
public int factorial(int num) { ... }

Dans cet exemple, nous avons la méthode factorielle standard que nous n'avons pas pris la peine d'implémenter. Cependant, ce qui le rend différent, ce sont les notes DbC dans le commentaire. En particulier, nous avons une précondition et une postcondition.

Condition préalable

Dans la condition préalable , nous spécifions ce qui doit être vrai concernant l'état de la classe et l'entrée pour que la méthode se comporte correctement. Dans ce cas, nous ne nous soucions pas de la classe puisqu'il s'agit probablement plus d'une méthode statique de toute façon.

Cela dit, nous nous soucions de ce qui est passé dans la méthode :

// Precondition: 0 <= num <= 12

D'un côté, cela n'a pas de sens de calculer une factorielle négative, nous le spécifions donc dans la condition préalable.

À l'autre extrémité, nous avons certaines limitations dans la taille d'un entier. Si nous acceptons des nombres trop grands, notre résultat sera bouclé. Nous ne voulons pas cela, nous demandons donc que les entrées ne soient jamais supérieures à 12.

Cela ne signifie pas que nous ne pouvons pas appeler la méthode factorielle avec des valeurs négatives ou des valeurs supérieures à 12. Nous indiquons que cela est une erreur sur l'appelant et non sur la méthode.

Post-condition

Pendant ce temps, la postcondition nous indique l'état de la sortie et de la classe après l'exécution de la méthode. Comme nous ne modifions aucune variable d'état, nous avons établi une règle sur la sortie attendue :

// Postcondition: return == num!

Dans ce cas, nous promettons que le résultat est la factorielle de l'entrée. Assez simple !

Programmation défensive

Pour être clair, DbC ne signifie pas que nous ignorons les entrées en dehors de notre ensemble de préconditions. En tant que bons programmeurs défensifs, nous veillerons à signaler les erreurs ou les exceptions pour toutes les mauvaises entrées.

De même, DbC ne garantit pas non plus que nous obtiendrons toujours un bon résultat sur nos méthodes. Les contrats eux-mêmes nous permettent simplement de commencer à constituer un régiment de test. Si nous savons à quoi nous attendre à chaque extrémité d'une méthode, nous pouvons alors commencer à les tester.

Pour plus d'informations, consultez la brève introduction d'UNC à la conception par contrat.

Principes de base des tests JUnit

Alors, qu'avons-nous couvert jusqu'à présent ?

Eh bien, nous avons commencé cette leçon avec un débogage de base. Pour commencer, nous avons examiné une méthode et déterminé son comportement attendu. Ensuite, nous avons analysé la solution et décomposé la méthode en ses branches.

Pour tester ces branches, nous avons sélectionné deux points de données, un pour chaque branche. Nous avons ensuite exécuté la méthode en utilisant chaque point de données et nous avons analysé les résultats. Les résultats ont indiqué qu'un seul des points de données fonctionnait réellement comme prévu.

À partir de là, nous avons exploité la déclaration d'impression préexistante pour avoir une idée de l'endroit où notre solution échouait. Une fois le problème identifié, nous avons retravaillé notre code et retesté les deux points de données.

Un retour en arrière

Après quelques débogages, nous avons couvert une leçon sur la conception par contrat.

Pour être clair, nous n'utiliserons généralement pas DbC au sens strict, mais le concept s'applique bien aux tests. En fait, pourquoi n'essayons-nous pas d'appliquer les principes DbC à la méthode que nous avons déboguée ? De cette façon, nous pourrons nous familiariser avec les règles avant de passer aux tests :

/**
 * Checks if the input is positive.
 *
 * Precondition: None
 * Postcondition: true if num > 0, false otherwise
 */
public static boolean isPositive(int num) {
    // Assume false
    boolean state = false;
 
    if (num > 0) {
        System.out.println("num is positive");
        state = true;
    }

    return state;
}

Ici, nous pouvons voir que nous ne faisons aucune hypothèse sur l'entrée. Nous acceptons avec plaisir toute la plage de valeurs entières en entrée. Quant à la postcondition, nous promettons que la sortie sera vraie pour les entiers supérieurs à 0 et fausse sinon.

Maintenant que nous connaissons nos préconditions et postconditions, nous savons exactement ce qu'il faut tester, et nous l'avons démontré lors du débogage.

Malheureusement, le code ne reste généralement pas intact. Plus tard, nous voudrons peut-être ajouter une autre clause qui spécifie le comportement de 0. Dans des cas comme ceux-ci, il est utile d'écrire des tests automatisés qui gèrent les tests de cohérence pour nous.

En d'autres termes, nous ne voulons pas avoir à vérifier manuellement que cette méthode fonctionne à chaque fois que nous apportons une modification.

Test à l'aide de la méthode principale

Heureusement, Java a une solution prête à l'emploi. C'est un framework appelé JUnit, et il nous permet d'écrire des méthodes de test. Mais comment écrit-on une méthode de test ? Avant de nous plonger dans la syntaxe, réfléchissons-y une seconde.

Auparavant, si on voulait tester une méthode manuellement, que faisait-on ? Dans un premier temps, nous avons essayé d'identifier quelques entrées pour tester les différentes branches d'une méthode. Ensuite, nous avons exécuté cette méthode en utilisant ces points de données. Dans le Dr Java, c'est trivial. Nous pouvons appeler la méthode directement depuis le volet des interactions en utilisant chaque point de données.

Cependant, si nous utilisons un IDE comme Eclipse, nous devrons peut-être écrire manuellement notre code de test dans la méthode principale. Ce n'est pas une façon très amusante de tester, mais cela fait le travail pour les petits projets. Essayons :

public class MyMathWorkshop {
    
    public static boolean isPositive(int num) {
        // Assume false
        boolean state = false;
 
        if (num > 0) {
            System.out.println("num is positive");
            state = true;
        }

        return state;
    }

    public static void main(String args[]) {
        boolean positiveTest = MyMathWorkshop.isPositive(5);
        boolean negativeTest = MyMathWorkshop.isPositive(-5);

        System.out.println("Positive Test: " + positiveTest);
        System.out.println("Negative Test: " + negativeTest);
    }
}

Après une course rapide, nous aurons nos résultats! Cependant, cette méthode de test est super fastidieuse et pas toujours possible. Heureusement, nous pouvons tirer parti des tests JUnit.

Présentation de JUnit

La beauté des tests JUnit est que tout le code de notre méthode principale peut être extrait dans une méthode de test spéciale. Mieux encore, nous pouvons échanger ces instructions d'impression contre des méthodes d'assertion spéciales. Ces méthodes d'assertion nous permettent de vérifier le résultat réel de notre appel de méthode par rapport à un résultat attendu. Par exemple :

assertTrue(MyMathWorkshop.isPositive(5));

Dans cette ligne, nous affirmons que isPositive(5) renvoie true . Si pour une raison quelconque isPositive(5) renvoie false , le test échouera. En passant, nous aurions pu rédiger le test comme suit :

boolean positiveTest = MyMathWorkshop.isPositive(5);
assertTrue(positiveTest);

Dans cet exemple, nous stockons explicitement le résultat de notre test dans une variable booléenne. Ensuite, nous transmettons cette variable à notre méthode de test.

Ce type de syntaxe est probablement ce que nous connaissons le mieux. Cependant, Java nous permet de sauter complètement l'étape de la variable locale. Au lieu de cela, nous pouvons passer un appel de méthode en tant que paramètre à une autre méthode comme indiqué dans le premier assertTrue exemple.

Les deux options sont valables, c'est donc vraiment une question de préférence. L'option 1 peut parfois être plus difficile à déboguer car les deux appels de méthode partagent la même ligne. Nous rencontrerons probablement ce problème lors du débogage du code à l'avenir.

Exemple JUnit

En tout cas, retour aux tests ! Maintenant, nous savons comment utiliser les tests JUnit sur nos méthodes. Allons-y et regardons un exemple de fichier de test pour notre MyMathWorkshop classe.

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;

import org.junit.Test;

public class MyMathWorkshopTest {
    
    @Test
    public void isPositiveTest() {
        assertTrue(MyMathWorkshop.isPositive(5));
        assertFalse(MyMathWorkshop.isPositive(-5));
    }
}

Il y a beaucoup de code ici que nous n'avons jamais vu auparavant. Pour commencer, notre méthode de test est accompagnée d'une annotation (@Test).

L'annotation sont des métadonnées que le framework de test JUnit utilise pour identifier les méthodes de test. En d'autres termes, nous devons marquer toutes nos méthodes de test avec le @Test annotation.

Pendant ce temps, en dehors de la classe, nous avons plusieurs instructions d'importation. Ces instructions nous donnent accès aux méthodes du framework de test JUnit. Il existe toute une liste de ces méthodes de test, mais les principales que nous utiliserons probablement sont assertTrue , assertFalse , et assertEquals .

Exécuter des cas de test

Dans DrJava, exécuter ces types de fichiers est aussi simple que d'appuyer sur le bouton de test après avoir compilé notre code. En cas de succès, nous devrions obtenir une liste de toutes les méthodes de test et de leurs résultats. Comme nous n'avons qu'une seule méthode de test, nous devrions voir un seul résultat de test réussi surligné en vert. Si le test échouait, la ligne serait surlignée en rouge.

D'autres IDE comme Eclipse font également un excellent travail d'intégration des tests au développement, mais nous approfondirons plus tard ces types d'outils.

Comme alternative, nous pouvons écrire des tests en utilisant le TestCase cadre. Ici, nous importons junit.framework.TestCase et étendre notre classe par elle. Cette méthode est un peu plus propre et nous oblige à suivre de bonnes conventions de nommage. Cependant, nous n'avons encore rien appris sur l'héritage, nous devrions donc éviter cette méthode pour l'instant.

Couverture des codes

À ce stade, nous devrions nous sentir assez à l'aise avec le test et le débogage du code. Toute la logique avec laquelle nous avons travaillé jusqu'à présent a été assez simple avec le cas occasionnel de branche, donc nous ne verrons peut-être pas toute la valeur de ce que nous avons appris aujourd'hui.

Cependant, à mesure que nous avançons, nous commencerons à aborder des concepts beaucoup plus compliqués tels que les boucles et les structures de données. Ensuite, nous devrons examiner la couverture du code pour nous assurer que nous prouvons réellement que nos méthodes font ce que nous voulons qu'elles fassent.

Couverture du code est une méthodologie logicielle qui donne la priorité aux tests qui traversent chaque ligne de code. Nous avons en fait atteint une couverture de branche de 100 % dans notre exemple de test JUnit ci-dessus. Si nous décidions d'ajouter notre méthode factorielle au mélange, nous aurions besoin d'écrire d'autres tests.

De nombreux IDE fournissent des outils d'analyse statique qui nous indiqueront en fait le pourcentage de code couvert par nos tests. De plus, ils nous diront quelles lignes manquent. En fait, Dr. Java prend désormais en charge la couverture de code comme l'une de ses dernières fonctionnalités.

J'ai hâte

Puisque nous avons couvert le débogage et les tests, nous devrions être prêts à relever un nouveau défi. Ensuite, nous allons nous attaquer aux boucles. Assurez-vous d'étudier toutes nos leçons précédentes, car bon nombre de ces sujets commenceront à s'appuyer les uns sur les autres. En fait, les boucles ajoutent un autre niveau pour contrôler le flux, nous voudrons donc certainement être plus à l'aise avec les conditions.

Pour l'instant, assurez-vous de partager cette leçon avec vos amis. Si vous appréciez vraiment cette série, pourquoi ne pas vous abonner à The Renegade Coder. De cette façon, vous ne manquerez plus jamais un autre article.


No
Balise Java