Les tests sont un aspect très important du développement et peuvent déterminer dans une large mesure le sort d'une application. De bons tests peuvent détecter les problèmes qui provoquent un crash précoce de votre application, mais des tests médiocres entraînent souvent des échecs et des temps d'arrêt à tout moment.
Bien qu'il existe trois principaux types de tests logiciels : les Tests unitaires, les tests fonctionnels et les tests d'intégration, dans cet article de blog, nous discuterons des tests unitaires au niveau du développeur. Avant d’entrer dans les détails, passons en revue les détails de chacun de ces trois tests.
Les tests unitaires sont utilisés pour tester des composants de code individuels et garantir que le code fonctionne comme prévu. Les tests unitaires sont écrits et exécutés par les développeurs. La plupart du temps, un framework de test comme JUnit ou TestNG est utilisé. Les cas de test sont généralement écrits au niveau de la méthode et exécutés via l'automatisation.
Les tests d'intégration vérifient si le système fonctionne dans son ensemble. Les tests d'intégration sont également effectués par les développeurs, mais au lieu de tester un seul composant, ils sont conçus pour tester plusieurs composants. Le système se compose de nombreux composants individuels tels que le code, la base de données, le serveur Web, etc. Les tests d'intégration peuvent révéler des problèmes tels que le câblage des composants, l'accès au réseau, les problèmes de base de données, etc.
Les tests fonctionnels vérifient si chaque fonctionnalité est correctement implémentée en comparant les résultats d'une entrée donnée à la spécification. En règle générale, cela ne se situe pas au niveau du développeur. Les tests fonctionnels sont effectués par une équipe de test distincte. Les cas de test sont rédigés sur la base de spécifications et les résultats réels sont comparés aux résultats attendus. Il existe plusieurs outils disponibles pour les tests fonctionnels automatisés, tels que Selenium et QTP.
Comme mentionné précédemment, les tests unitaires aident les développeurs à déterminer si le code fonctionne correctement. Dans cet article de blog, je fournirai des conseils utiles pour les tests unitaires en Java.
Java fournit plusieurs frameworks pour les tests unitaires. TestNG et JUnit sont les frameworks de test les plus populaires. Quelques fonctionnalités importantes de JUnit et TestNG :
Facile à configurer et à exécuter.
Prend en charge les commentaires.
Permet d'ignorer ou de regrouper et d'exécuter certains tests ensemble.
prend en charge les tests paramétrés, c'est-à-dire l'exécution de tests unitaires en spécifiant différentes valeurs au moment de l'exécution.
Prend en charge l'exécution automatisée des tests en s'intégrant à des outils de build tels que Ant, Maven et Gradle.
EasyMock est un framework moqueur qui complète les frameworks de tests unitaires tels que JUnit et TestNG. EasyMock lui-même n'est pas un framework complet. Cela ajoute simplement la possibilité de créer des objets fictifs pour des tests plus faciles. Par exemple, une méthode que nous souhaitons tester peut appeler une classe DAO qui récupère les données d'une base de données. Dans ce cas, EasyMock peut être utilisé pour créer un MockDAO qui renvoie des données codées en dur. Cela nous permet de tester facilement nos méthodes prévues sans avoir à nous soucier de l'accès à la base de données.
Le développement piloté par les tests (TDD) est un processus de développement logiciel dans lequel nous écrivons des tests basés sur les exigences avant de commencer tout codage. Puisqu’il n’y a pas encore de codage, le test échouera dans un premier temps. Écrivez ensuite la quantité minimale de code pour réussir le test. Refactorisez ensuite le code jusqu'à ce qu'il soit optimisé.
L'objectif est d'écrire des tests qui couvrent toutes les exigences, plutôt que d'écrire du code qui pourrait même ne pas répondre aux exigences. TDD est génial car il donne un code modulaire simple et facile à maintenir. La vitesse globale de développement est accélérée et les défauts sont facilement détectés. De plus, les tests unitaires sont créés comme un sous-produit de l'approche TDD.
Cependant, le TDD peut ne pas convenir à toutes les situations. Dans les projets aux conceptions complexes, se concentrer sur la conception la plus simple pour faciliter la réussite des cas de test sans anticiper peut conduire à d’énormes changements de code. De plus, les méthodes TDD sont difficiles à utiliser pour les systèmes qui interagissent avec des systèmes existants, des applications GUI ou des applications qui fonctionnent avec des bases de données. De plus, les tests doivent être mis à jour à mesure que le code change.
Par conséquent, avant de décider d'adopter l'approche TDD, les facteurs ci-dessus doivent être pris en compte et des mesures doivent être prises en fonction de la nature du projet.
La couverture du code mesure (exprimée en pourcentage) la quantité de code exécutée lors de l'exécution de tests unitaires. Généralement, le code avec une couverture élevée a moins de chances de contenir des bogues non détectés, car une plus grande partie de son code source est exécutée pendant les tests. Voici quelques bonnes pratiques pour mesurer la couverture de code :
Utilisez un outil de couverture de code tel que Clover, Corbetura, JaCoCo ou Sonar. L'utilisation d'outils peut améliorer la qualité de vos tests car ils peuvent signaler les zones de votre code qui ne sont pas testées, vous permettant ainsi de développer des tests supplémentaires pour couvrir ces domaines.
Chaque fois qu'une nouvelle fonctionnalité est écrite, écrivez immédiatement une nouvelle couverture de test.
Assurez-vous qu'il existe des cas de test couvrant toutes les branches du code, c'est-à-dire les instructions if/else.
Une couverture de code élevée ne garantit pas des tests parfaits, alors soyez prudent !
La méthode concat ci-dessous accepte une valeur booléenne en entrée et transmet en outre deux chaînes uniquement si la valeur booléenne est vraie :
public String concat(boolean append, String a,String b) { String result = null; If (append) { result = a + b; } return result.toLowerCase(); }
Ce qui suit est la méthode ci-dessus Cas de test pour :
@Test public void testStringUtil() { String result = stringUtil.concat(true, "Hello ", "World"); System.out.println("Result is "+result); }
Dans ce cas, la valeur du test exécuté est vraie. Une fois le test exécuté, il réussira. Lorsque l'outil de couverture de code est exécuté, il affichera une couverture de code à 100 % car tout le code de la méthode concat a été exécuté. Cependant, si le test s'exécute avec une valeur false, une NullPointerException sera levée. Ainsi, une couverture de code à 100 % ne signifie pas vraiment que les tests couvrent tous les scénarios, ni que les tests sont bons.
Avant JUnit4, les données du scénario de test à exécuter devaient être codées en dur dans le scénario de test. Cela entraîne une limitation : pour exécuter des tests avec des données différentes, le code du scénario de test doit être modifié. Cependant, JUnit4 ainsi que TestNG prennent en charge l'externalisation des données de test afin que les cas de test puissent être exécutés sur différents ensembles de données sans modifier le code source.
La classe MathChecker suivante a des méthodes pour vérifier si un nombre est impair :
public class MathChecker { public Boolean isOdd(int n) { if (n%2 != 0) { return true; } else { return false; } } }
Ce qui suit est le scénario de test TestNG pour la classe MathChecker :
public class MathCheckerTest { private MathChecker checker; @BeforeMethod public void beforeMethod() { checker = new MathChecker(); } @Test @Parameters("num") public void isOdd(int num) { System.out.println("Running test for "+num); Boolean result = checker.isOdd(num); Assert.assertEquals(result, new Boolean(true)); } }
Ce qui suit est testng.xml (le fichier de configuration de TestNG) qui contient les données pour lesquelles les tests doivent être exécutés :
<?xml version="1.0" encoding="UTF-8"?> <suite name="ParameterExampleSuite" parallel="false"> <test name="MathCheckerTest"> <classes> <parameter name="num" value="3"></parameter> <class name="com.stormpath.demo.MathCheckerTest"/> </classes> </test> <test name="MathCheckerTest1"> <classes> <parameter name="num" value="7"></parameter> <class name="com.stormpath.demo.MathCheckerTest"/> </classes> </test> </suite>
Comme on peut le voir, dans ce cas les tests seront exécutés deux fois, une fois chacun pour les valeurs 3 et 7. En plus de spécifier les données de test via des fichiers de configuration XML, les données de test peuvent également être fournies dans les classes via les annotations DataProvider.
Semblable à TestNG, les données de test peuvent également être externalisées pour être utilisées avec JUnit. Ce qui suit est un cas de test JUnit pour la même classe MathChecker que ci-dessus :
@RunWith(Parameterized.class) public class MathCheckerTest { private int inputNumber; private Boolean expected; private MathChecker mathChecker; @Before public void setup(){ mathChecker = new MathChecker(); } // Inject via constructor public MathCheckerTest(int inputNumber, Boolean expected) { this.inputNumber = inputNumber; this.expected = expected; } @Parameterized.Parameters public static Collection<Object[]> getTestData() { return Arrays.asList(new Object[][]{ {1, true}, {2, false}, {3, true}, {4, false}, {5, true} }); } @Test public void testisOdd() { System.out.println("Running test for:"+inputNumber); assertEquals(mathChecker.isOdd(inputNumber), expected); } }
Comme on peut le voir, les données de test sur lesquelles le test doit être effectué sont spécifiées par la méthode getTestData(). Cette méthode peut être facilement modifiée pour lire les données à partir d'un fichier externe au lieu de coder en dur les données.
De nombreux développeurs novices ont l'habitude d'écrire des instructions System.out.println après chaque ligne de code pour vérifier si le code est exécuté correctement. Cette pratique s'étend souvent aux tests unitaires, ce qui entraîne un code de test encombré. Outre la confusion, cela nécessite une intervention manuelle du développeur pour vérifier le résultat imprimé sur la console afin de vérifier si le test s'est exécuté avec succès. Une meilleure approche consiste à utiliser des assertions qui indiquent automatiquement les résultats des tests.
La classe StringUti ci-dessous est une classe simple avec une méthode qui connecte deux caractères d'entrée et renvoie le résultat :
public class StringUtil { public String concat(String a,String b) { return a + b; } }
Voici deux des méthodes ci-dessus Tests unitaires :
@Test public void testStringUtil_Bad() { String result = stringUtil.concat("Hello ", "World"); System.out.println("Result is "+result); } @Test public void testStringUtil_Good() { String result = stringUtil.concat("Hello ", "World"); assertEquals("Hello World", result); }
testStringUtil_Bad sera toujours réussi car il n'a aucune assertion. Les développeurs doivent vérifier manuellement la sortie du test sur la console. testStringUtil_Good échouera si la méthode renvoie des résultats incorrects et ne nécessite pas l'intervention du développeur.
Certaines méthodes n'ont pas de résultats déterministes, c'est-à-dire que le résultat de la méthode n'est pas connu à l'avance et peut changer à chaque fois. Par exemple, considérons le code suivant, qui possède une fonction complexe et une méthode qui calcule le temps en millisecondes nécessaire pour exécuter la fonction complexe :
public class DemoLogic { private void veryComplexFunction(){ //This is a complex function that has a lot of database access and is time consuming //To demo this method, I am going to add a Thread.sleep for a random number of milliseconds try { int time = (int) (Math.random()*100); Thread.sleep(time); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public long calculateTime(){ long time = 0; long before = System.currentTimeMillis(); veryComplexFunction(); long after = System.currentTimeMillis(); time = after - before; return time; } }
Dans ce cas, chaque Chaque fois que la méthode calculateTime est exécutée, elle renvoie une valeur différente. L'écriture de cas de test pour cette méthode ne sera d'aucune utilité puisque le résultat de cette méthode est variable. Par conséquent, la méthode de test ne sera pas en mesure de vérifier le résultat d’une exécution particulière.
En règle générale, les développeurs consacrent beaucoup de temps et d'efforts à l'écriture de cas de test pour s'assurer que l'application fonctionne comme prévu. Cependant, il est également important de tester les cas de tests négatifs. Les scénarios de test négatifs font référence à des scénarios de test qui testent si le système peut gérer des données non valides. Par exemple, considérons une fonction simple qui lit une valeur alphanumérique de longueur 8, saisie par l'utilisateur. En plus des valeurs alphanumériques, les cas de test négatifs suivants doivent être testés :
Valeurs non alphanumériques spécifiées par l'utilisateur telles que les caractères spéciaux.
Valeur nulle spécifiée par l'utilisateur.
Valeur spécifiée par l'utilisateur supérieure ou inférieure à 8 caractères.
De même, les cas de tests limites testent si le système est adapté aux valeurs extrêmes. Par exemple, si l'utilisateur souhaite saisir une valeur numérique comprise entre 1 et 100, alors 1 et 100 sont les valeurs limites et il est très important de tester le système pour ces valeurs.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!