JavaRush /Blog Java /Random-FR /Tests unitaires Java : techniques, concepts, pratique

Tests unitaires Java : techniques, concepts, pratique

Publié dans le groupe Random-FR
Aujourd'hui, il est difficile de trouver une application qui ne soit pas couverte par des tests, ce sujet sera donc plus que jamais d'actualité pour les développeurs débutants : sans tests, vous ne pouvez aller nulle part. En guise de publicité, je vous suggère de consulter mes articles passés. Certains d’entre eux couvrent des tests (et malgré tout les articles seront très utiles) :
  1. Test d'intégration d'une base de données utilisant MariaDB pour remplacer MySql
  2. Mise en place d'une application multilingue
  3. Enregistrement des fichiers dans l'application et des données les concernant dans la base de données
Examinons quels types de tests sont utilisés en principe, puis nous étudierons en détail tout ce que vous devez savoir sur les tests unitaires.

Types de tests

Qu'est-ce qu'un examen ? Comme le dit Wiki : « Un test ou un test est un moyen d'étudier les processus sous-jacents d'un système en plaçant le système dans différentes situations et en suivant les changements observables dans celui-ci. » En d'autres termes, il s'agit d'un test du bon fonctionnement de notre système dans certaines situations. Tout sur les tests unitaires : méthodes, concepts, pratique - 2Eh bien, voyons quels types de tests il existe :
  1. Les tests unitaires sont des tests dont la tâche est de tester chaque module du système individuellement. Il est souhaitable qu'il s'agisse d'éléments du système peu divisibles, par exemple des modules.

  2. Les tests système sont un test de haut niveau permettant de tester le fonctionnement d'un élément plus important d'une application ou du système dans son ensemble.

  3. Les tests de régression sont des tests utilisés pour vérifier si de nouvelles fonctionnalités ou corrections de bugs affectent les fonctionnalités existantes de l'application et si d'anciens bugs réapparaissent.

  4. Les tests fonctionnels consistent à vérifier la conformité d'une partie de l'application aux exigences énoncées dans les spécifications, les user stories, etc.

    Types de tests fonctionnels :

    • test « boîte blanche » de conformité d’une partie de l’application aux exigences avec connaissance de la mise en œuvre interne du système ;
    • test « boîte noire » de conformité d’une partie de l’application aux exigences sans connaissance de la mise en œuvre interne du système.
  5. Les tests de performances sont un type de tests écrits pour déterminer la vitesse à laquelle un système ou une partie de celui-ci s'exécute sous une certaine charge.
  6. Tests de charge - tests conçus pour vérifier la stabilité du système sous des charges standard et pour trouver le pic maximum possible auquel l'application fonctionne correctement.
  7. Les tests de résistance sont un type de test conçu pour vérifier la fonctionnalité d'une application sous des charges non standard et pour déterminer le pic maximum possible auquel le système ne tombera pas en panne.
  8. Tests de sécurité - tests utilisés pour vérifier la sécurité d'un système (contre les attaques de pirates informatiques, de virus, d'accès non autorisé à des données confidentielles et autres plaisirs de la vie).
  9. Les tests de localisation sont des tests de localisation pour une application.
  10. Les tests d'utilisabilité sont un type de test visant à vérifier la convivialité, la compréhensibilité, l'attractivité et la capacité d'apprentissage pour les utilisateurs.
  11. Tout cela semble bien, mais comment cela fonctionne-t-il en pratique ? C'est simple : la pyramide de test de Mike Cohn est utilisée : Tout sur les tests unitaires : techniques, concepts, pratique - 4il s'agit d'une version simplifiée de la pyramide : elle est désormais divisée en parties plus petites. Mais aujourd’hui, nous ne pervertirons pas et considérerons l’option la plus simple.
    1. Unit - tests unitaires utilisés dans différentes couches de l'application, testant la plus petite logique divisible de l'application : par exemple, une classe, mais le plus souvent une méthode. Ces tests tentent généralement de s'isoler autant que possible de la logique externe, c'est-à-dire de créer l'illusion que le reste de l'application fonctionne en mode standard.

      Il devrait toujours y avoir beaucoup de ces tests (plus que les autres types), car ils testent de petits éléments et sont très légers, ne consommant pas beaucoup de ressources (par ressources, j'entends RAM et temps).

    2. Intégration - tests d'intégration. Il vérifie des éléments plus importants du système, c'est-à-dire qu'il s'agit soit d'une combinaison de plusieurs éléments de logique (plusieurs méthodes ou classes), soit de l'exactitude du travail avec un composant externe. Il y a généralement moins de tests de ce type que de tests unitaires, car ils sont plus lourds.

      À titre d'exemple de tests d'intégration, vous pouvez envisager de vous connecter à une base de données et de vérifier que les méthodes qui l'utilisent fonctionnent correctement .

    3. UI - tests qui vérifient le fonctionnement de l'interface utilisateur. Ils affectent la logique à tous les niveaux de l'application, c'est pourquoi ils sont également appelés de bout en bout. En règle générale, ils sont beaucoup moins nombreux, car ils sont les plus lourds et doivent vérifier les chemins les plus nécessaires (utilisés).

      Dans la figure ci-dessus, nous voyons le rapport des aires des différentes parties du triangle : approximativement la même proportion est maintenue dans le nombre de ces tests en travail réel.

      Aujourd'hui, nous allons examiner de plus près les tests les plus utilisés : les tests unitaires, car tout développeur Java qui se respecte devrait pouvoir les utiliser à un niveau basique.

    Concepts clés des tests unitaires

    La couverture des tests (Code Coverage) est l'une des principales évaluations de la qualité des tests d'application. Il s'agit du pourcentage de code couvert par les tests (0-100 %). Dans la pratique, beaucoup de gens recherchent ce pourcentage, avec lequel je ne suis pas d'accord, puisqu'ils commencent à ajouter des tests là où ils ne sont pas nécessaires. Par exemple, notre service propose des opérations CRUD standard (créer/obtenir/mettre à jour/supprimer) sans logique supplémentaire. Ces méthodes sont purement intermédiaires qui délèguent le travail à la couche qui travaille avec le référentiel. Dans cette situation, nous n'avons rien à tester : peut-être si cette méthode fait appel à une méthode du Tao, mais ce n'est pas grave. Pour évaluer la couverture des tests, des outils supplémentaires sont généralement utilisés : JaCoCo, Cobertura, Clover, Emma, ​​​​etc. Pour une étude plus détaillée de cette question, conservez quelques articles appropriés : TDD (Test-driven development) - développement piloté par les tests. Dans cette approche, tout d’abord, un test est écrit pour vérifier un code spécifique. Il s’avère qu’il s’agit d’un test en boîte noire : nous savons ce qui se passe à l’entrée et nous savons ce qui devrait se passer à la sortie. Cela évite la duplication de code. Le développement piloté par les tests commence par la conception et le développement de tests pour chaque petite fonctionnalité de l'application. Dans l'approche TDD, tout d'abord, un test est développé qui définit et vérifie ce que fera le code. L'objectif principal de TDD est de rendre le code plus clair, plus simple et sans erreur. Tout sur les tests unitaires : méthodes, concepts, pratique - 6L’approche comprend les éléments suivants :
    1. Nous écrivons notre test.
    2. Nous effectuons le test, qu'il soit réussi ou non (on voit que tout est rouge - ne paniquez pas : c'est comme ça que ça devrait être).
    3. Nous ajoutons le code qui doit satisfaire ce test (lancez le test).
    4. Nous refactorisons le code.
    Partant du fait que les tests unitaires sont les plus petits éléments de la pyramide d'automatisation des tests, TDD est basé sur eux. À l'aide de tests unitaires, nous pouvons tester la logique métier de n'importe quelle classe. BDD (Behavior-driven development) - développement par le comportement. Cette approche est basée sur TDD. Plus précisément, il utilise des exemples écrits en langage clair (généralement en anglais) qui illustrent le comportement du système pour toutes les personnes impliquées dans le développement. Nous n’approfondirons pas ce terme, puisqu’il concerne principalement les testeurs et les analystes métiers. Cas de test - un script qui décrit les étapes, les conditions spécifiques et les paramètres nécessaires pour vérifier la mise en œuvre du code testé. Le luminaire est un état de l’environnement de test nécessaire à l’exécution réussie de la méthode testée. Il s'agit d'un ensemble prédéterminé d'objets et de leur comportement dans les conditions utilisées.

    Étapes de test

    Le test comprend trois étapes :
    1. Spécifier les données à tester (fixations).
    2. Utilisation du code testé (appel de la méthode testée).
    3. Vérifier les résultats et les comparer avec ceux attendus.
    Tout sur les tests unitaires : méthodes, concepts, pratique - 7Pour garantir la modularité des tests, vous devez être isolé des autres couches de l'application. Cela peut être fait en utilisant des talons, des moqueurs et des espions. Les simulations sont des objets personnalisables (par exemple, spécifiques à chaque test) et vous permettent de définir des attentes pour les appels de méthode sous la forme de réponses que nous prévoyons de recevoir. Les vérifications des attentes sont effectuées via des appels à des objets Mock. Stubs : fournissent une réponse câblée aux appels pendant les tests. Ils peuvent également stocker des informations sur l'appel (par exemple, des paramètres ou le nombre de ces appels). Ceux-ci sont parfois appelés par leur propre terme – espion ( Spy ). Parfois, ces termes stubs et mock sont confondus : la différence est qu'un stub ne vérifie rien, mais simule seulement un état donné. Une maquette est un objet qui a des attentes. Par exemple, qu’une méthode de classe donnée doit être appelée un certain nombre de fois. En d’autres termes, votre test ne sera jamais interrompu à cause d’un stub, mais il pourrait être interrompu à cause d’une simulation.

    Environnements de test

    Alors maintenant, passons aux choses sérieuses. Il existe plusieurs environnements de test (frameworks) disponibles pour Java. Les plus populaires d'entre eux sont JUnit et TestNG. Pour notre examen, nous utilisons : Tout sur les tests unitaires : méthodes, concepts, pratique - 8Un test JUnit est une méthode contenue dans une classe qui est utilisée uniquement à des fins de test. Une classe porte généralement le même nom que la classe qu'elle teste avec +Test à la fin. Par exemple, CarService → CarServiceTest. Le système de build Maven inclut automatiquement ces classes dans la zone de test. En fait, cette classe est appelée classe de test. Passons un peu en revue les annotations de base : @Test - définition de cette méthode comme méthode de test (en fait, la méthode marquée de cette annotation est un test unitaire). @Before - marque la méthode qui sera exécutée avant chaque test. Par exemple, remplir les données de test de classe, lire les données d'entrée, etc. @After - placé au-dessus de la méthode qui sera appelée après chaque test (nettoyage des données, restauration des valeurs par défaut). @BeforeClass - placé au-dessus de la méthode - analogue à @Before. Mais cette méthode n'est appelée qu'une seule fois avant tous les tests pour une classe donnée et doit donc être statique. Il est utilisé pour effectuer des opérations plus lourdes, telles que la levée d'une base de données de test. @AfterClass est l'opposé de @BeforeClass : exécuté une fois pour une classe donnée, mais exécuté après tous les tests. Utilisé, par exemple, pour nettoyer les ressources persistantes ou se déconnecter de la base de données. @Ignore - note que la méthode ci-dessous est désactivée et sera ignorée lors de l'exécution globale des tests. Il est utilisé dans différents cas, par exemple si la méthode de base a été modifiée et que l'on n'a pas eu le temps de refaire le test correspondant. Dans de tels cas, il est également conseillé d'ajouter une description - @Ignore("Some description"). @Test (attendu = Exception.class) - utilisé pour les tests négatifs. Ce sont des tests qui vérifient comment une méthode se comporte en cas d'erreur, c'est-à-dire que le test s'attend à ce que la méthode lève une exception. Une telle méthode est désignée par l'annotation @Test, mais avec une erreur à détecter. @Test(timeout=100) - vérifie que la méthode s'exécute en 100 millisecondes maximum. @Mock - une classe est utilisée sur un champ pour définir un objet donné comme simulé (cela ne vient pas de la bibliothèque Junit, mais de Mockito), et si nous en avons besoin, nous définirons le comportement du simulacre dans une situation spécifique , directement dans la méthode de test. @RunWith(MockitoJUnitRunner.class) - la méthode est placée au-dessus de la classe. C'est le bouton pour y exécuter des tests. Les coureurs peuvent être différents : par exemple, il y a les suivants : MockitoJUnitRunner, JUnitPlatform, SpringRunner, etc.). Dans JUnit 5, l'annotation @RunWith a été remplacée par l'annotation @ExtendWith plus puissante. Jetons un coup d'œil à quelques méthodes pour comparer les résultats :
    • assertEquals(Object expecteds, Object actuals)— vérifie si les objets transmis sont égaux.
    • assertTrue(boolean flag)— vérifie si la valeur transmise renvoie vrai.
    • assertFalse(boolean flag)— vérifie si la valeur transmise renvoie false.
    • assertNull(Object object)– vérifie si l'objet est nul.
    • assertSame(Object firstObject, Object secondObject)— vérifie si les valeurs transmises font référence au même objet.
    • assertThat(T t, Matcher<T> matcher)— vérifie si t satisfait à la condition spécifiée dans le matcher.
    Il existe également un formulaire de comparaison utile de assertj - assertThat(firstObject).isEqualTo(secondObject) Ici, j'ai parlé des méthodes de base, car les autres sont des variantes différentes de celles ci-dessus.

    Pratique des tests

    Examinons maintenant le matériel ci-dessus à l'aide d'un exemple spécifique. Nous allons tester la méthode pour le service - mise à jour. Nous ne considérerons pas la couche dao, car c'est notre couche par défaut. Ajoutons un starter pour les tests :
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <version>2.2.2.RELEASE</version>
       <scope>test</scope>
    </dependency>
    Donc, la classe de service :
    @Service
    @RequiredArgsConstructor
    public class RobotServiceImpl implements RobotService {
       private final RobotDAO robotDAO;
    
       @Override
       public Robot update(Long id, Robot robot) {
           Robot found = robotDAO.findById(id);
           return robotDAO.update(Robot.builder()
                   .id(id)
                   .name(robot.getName() != null ? robot.getName() : found.getName())
                   .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
                   .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
                   .build());
       }
    }
    8 - extraire l'objet mis à jour de la base de données 9-14 - créer l'objet via le constructeur, si l'objet entrant a un champ - définissez-le, sinon - laissez ce qui est dans la base de données Et regardez notre test :
    @RunWith(MockitoJUnitRunner.class)
    public class RobotServiceImplTest {
       @Mock
       private RobotDAO robotDAO;
    
       private RobotServiceImpl robotService;
    
       private static Robot testRobot;
    
       @BeforeClass
       public static void prepareTestData() {
           testRobot = Robot
                   .builder()
                   .id(123L)
                   .name("testRobotMolly")
                   .cpu("Intel Core i7-9700K")
                   .producer("China")
                   .build();
       }
    
       @Before
       public void init() {
           robotService = new RobotServiceImpl(robotDAO);
       }
    1 — notre Runner 4 — isoler le service de la couche dao en remplaçant un mock 11 — définir une entité de test pour la classe (celle que nous utiliserons comme hamster de test) 22 — définir un objet de service que nous testerons
    @Test
    public void updateTest() {
       when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
       when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
       Robot robotForUpdate = Robot
               .builder()
               .name("Vally")
               .cpu("AMD Ryzen 7 2700X")
               .build();
    
       Robot resultRobot = robotService.update(123L, robotForUpdate);
    
       assertNotNull(resultRobot);
       assertSame(resultRobot.getId(),testRobot.getId());
       assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
       assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
       assertEquals(resultRobot.getProducer(),testRobot.getProducer());
    }
    Nous voyons ici une division claire du test en trois parties : 3-9 - configuration des appareils 11 - exécution de la partie testée 13-17 - vérification des résultats Plus de détails : 3-4 - définition du comportement de moka dao 5 - définition d'une instance que nous mettrons à jour en plus de notre standard 11 - utiliser la méthode et prendre l'instance résultante 13 - vérifier qu'elle n'est pas zéro 14 - vérifier l'ID du résultat et les arguments de la méthode spécifiés 15 - vérifier si le nom a été mis à jour 16 - regarder au résultat par le processeur 17 - puisque nous ne l'avons pas défini dans le champ de l'instance de mise à jour, il devrait rester le même, vérifions-le. Tout sur les tests unitaires : méthodes, concepts, pratique - 9Lançons : Tout sur les tests unitaires : techniques, concepts, pratique - 10le test est vert, vous pouvez expirer)) Alors, résumons : les tests améliorent la qualité du code et rendent le processus de développement plus flexible et fiable. Imaginez combien d'efforts nous devrions consacrer à la refonte d'un logiciel contenant des centaines de fichiers de classe. Une fois que nous avons écrit des tests unitaires pour toutes ces classes, nous pouvons refactoriser en toute confiance. Et surtout, cela nous aide à trouver facilement les erreurs lors du développement. Les gars, c'est tout pour moi aujourd'hui : versez des likes, écrivez des commentaires))) Tout sur les tests unitaires : méthodes, concepts, pratique - 11
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION