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 : Tests unitaires, tests fonctionnels et 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.
1. Utiliser des frameworks pour les tests unitaires
2. Utilisez le développement piloté par les tests avec prudence !
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
modulairesimple 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.
3. Mesurer la couverture du code
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 Test cas pour la méthode ci-dessus :
@Test public void testStringUtil() { String result = stringUtil.concat(true, "Hello ", "World"); System.out.println("Result is "+result); }
Dans ce cas, la valeur de l'exécution du test 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, NullPointerException
sera lancé. 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înait une limitation : pour exécuter des tests avec des données différentes, le code du scénario de test devait ê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 effectué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, le test sera exécuté deux fois, une fois chacune 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 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 StringUti
classe suivante 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; } }
Ce qui suit est la méthode ci-dessus Les deux 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 seront toujours réussis 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 , il renverra une valeur différente à chaque fois que la méthode calculateTime
sera exécutée. 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 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 des 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!