Writing tests

Reading time5 min

En bref

Résumé de l’article

Dans cet article, nous introduisons les tests, qui visent à vérifier que les codes développés se comportent comme prévu. L’automatisation de l’exécution des tests est également utile pour vérifier qu’une nouvelle fonctionnalité dans un code n’a pas cassé quelque chose dans un code déjà existant.

Nous mentionnons aussi comment concevoir ces tests, et montrons des exemples avec des bibliothèques dédiées.

Points importants à retenir

  • Les tests et la documentation ensemble assurent la robustesse et la pérennité du logiciel à long terme.

  • Écrire des tests garantit que le logiciel fonctionne comme attendu et identifie rapidement les bugs et régressions.

  • Cela encourage les développeurs à réfléchir de manière critique à la conception et aux fonctionnalités du code, conduisant à un code plus propre, plus fiable et robuste.

  • Les tests automatisés facilitent la maintenance en détectant rapidement les problèmes à corriger.

  • Les tests automatisés sont intégrés aux pipelines d’intégration continue (CI) et de déploiement continu (CD), garantissant que les nouvelles modifications sont systématiquement testées et intégrées sans introduire de nouveaux problèmes.

Contenu de l’article

1 — Tester votre code

1.1 — Le but des tests

Écrire des tests pour le code est une pratique essentielle en développement logiciel, car cela garantit que chaque partie du programme fonctionne comme prévu.
Les tests automatisés, qu’ils soient unitaires, d’intégration ou système, détectent rapidement les bugs et régressions, facilitant la maintenance et améliorant la qualité globale du logiciel.

Au-delà de la détection d’erreurs, les tests renforcent la confiance des développeurs lors des modifications du code et rendent le processus de refactoring plus fluide.
De plus, des tests complets favorisent une meilleure conception du code en encourageant les développeurs à considérer les cas limites et les points de défaillance potentiels dès le début du développement.
Ils soutiennent également les pratiques d’intégration et de déploiement continus (CI/CD), garantissant que les nouvelles modifications de code sont rigoureusement vérifiées avant d’être intégrées dans la base de code principale.

En résumé, écrire des tests est une mesure proactive qui améliore la fiabilité, la maintenabilité et l’efficacité du développement logiciel.

1.2 — Quel type de test ?

Il existe plusieurs types de tests utilisés en développement logiciel, chacun servant un but spécifique et ciblant différents aspects du logiciel.
Voici quelques types courants de tests :

  • Tests unitaires – Les tests unitaires se concentrent sur le test des unités ou composants individuels du logiciel, tels que des fonctions, méthodes ou classes, isolément du reste de l’application.
    Ils vérifient que chaque unité fonctionne correctement selon sa spécification.

  • Tests d’intégration – Les tests d’intégration vérifient que différentes unités ou composants du logiciel fonctionnent correctement ensemble lorsqu’ils sont intégrés.
    Ils testent les interactions entre ces unités et s’assurent qu’elles communiquent et collaborent comme prévu.

  • Tests de régression – Les tests de régression visent à garantir que les modifications apportées à la base de code, telles que les corrections de bugs ou les nouvelles fonctionnalités, n’introduisent pas de nouveaux défauts ou régressions dans le logiciel.
    Ils aident à maintenir la stabilité et la fiabilité de l’application au fil du temps.

  • Tests de sécurité – Les tests de sécurité évaluent la sécurité du logiciel et identifient les vulnérabilités ou faiblesses potentielles qui pourraient être exploitées par des attaquants.
    Ils aident à garantir que le logiciel est protégé contre les menaces et violations de sécurité.

En utilisant une combinaison de ces tests tout au long du cycle de vie du développement logiciel, les équipes peuvent s’assurer que le logiciel répond aux normes de qualité, fonctionne de manière fiable et satisfait les besoins des utilisateurs.

2 — Tests unitaires

Dans cet article, nous nous concentrerons sur les tests unitaires.

Un test unitaire est un type de test logiciel qui se concentre sur la vérification de la correction des unités individuelles de code.
Une “unité” est la plus petite partie testable d’une application, telle qu’une fonction, méthode ou classe.
L’objectif principal des tests unitaires est de s’assurer que chaque unité fonctionne comme prévu, isolément du reste de l’application.

Écrire des tests peut sembler un processus lent à première vue, mais ils sont essentiels pour livrer un programme à temps et dans le budget.
Les tests aident à détecter les bugs tôt dans le cycle de développement, réduisant le coût de correction des erreurs.
Ils permettent également de refactoriser le code en toute confiance et facilitent la compréhension du code.
Les frameworks de test permettent d’écrire des tests rapidement.

2.1 — Bibliothèques spécialisées : unittest et JUnit

Comme indiqué dans la documentation de unittest, unittest et JUnit sont proches :

Le framework de test unitaire unittest a été initialement inspiré par JUnit et a une saveur similaire aux principaux frameworks de test unitaire dans d’autres langages.

Pour voir comment construire des tests, voici une fonction qui inverse la capitalisation d’un mot :

def inverse_capitalization (word: str) -> str:

    """
        Inverts the capitalization of a word.
        For instance Hello should be transformed to hELLO.
        In:
            * word: The word to process.
        Out:
            * The word with inversed capitalization.
    """

    # Fill a list char by char
    result = []
    for char in word:
        result.append(char.lower() if char.isupper() else char.upper())
    
    # Recreate the string
    return ''.join(result)
public String inverseCapitalization(String word){
    char[] result = new char[word.length()];
    for (int i = 0; i < word.length(); i++) {
        char c = word.charAt(i);
        result[i] = Character.isUpperCase(c) ? Character.toLowerCase(c) : Character.toUpperCase(c);
    }
    return new String(result);
}

Pour s’assurer que la méthode fait ce qu’on attend d’elle, une classe de test a été écrite.

Information

Vous n’avez pas encore rencontré beaucoup de classes.
Une classe est un type de données abstrait, qui regroupe à la fois des attributs (données) et des méthodes (fonctions).

Pour définir une classe, vous devez définir ces éléments dans un bloc class.
Plus de détails seront donnés à ce sujet dans la session de programmation dédiée.
Pour l’instant, considérez simplement que vous devez écrire vos tests comme dans l’exemple ci-dessous.

# Needed imports
import unittest



# Define a class for all your tests of the unit you want to test
# Here, the unit is a single function, but it could be an entire module, or a class
# The name of your test class (here, TestInverseCapitalization) should represent what you will test
# For the particular case of unit tests, it should inherit from unittest.TestCase as follows
# This allows access to useful methods such as 'assertEqual'
class TestInverseCapitalization (unittest.TestCase):

    # Define a method that will test the method for uppercase inputs
    # The name of the method should be representative of the test, and start with test_
    # Also, as it is a method in a class, it should have 'self' as first argument
    def test_lower (self):

        # For tests, it is good to work with assertions
        # Here, assertEqual is provided by the unittest library
        # It is nearly equivalent to: assert 'HELLO!' == inverse_capitalization('hello!')
        # The library provides other methods for complex assertions
        self.assertEqual('HELLO!', inverse_capitalization('hello!'))

    # Another method for another test
    def test_upper (self):
        self.assertEqual('hello!', inverse_capitalization('HELLO!'))

    # Another method for another test
    def test_mix (self):
        self.assertEqual('hElLo!', inverse_capitalization('HeLlO!'))    



# In the main, you can then ask unittest to run all your defined tests
if __name__ == '__main__':
    _ = unittest.main(argv=[""], verbosity=2, exit=False)
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestInverseCapitalization {
    
    @Test
    public void testUpper() {
        Assertions.assertEquals("hello!", inverseCapitalization("HELLO!"));
    }
    
    @Test
    public void testLower() {
        Assertions.assertEquals("HELLO!", inverseCapitalization("hello!"));
    }

    @Test
    public void testMix() {
        Assertions.assertEquals("HeLlO!", inverseCapitalization("hElLo!"));
    }
}

Cette classe de tests, appelée TestInverseCapitalization, contient trois tests.
Un seul concept est évalué par test et un seul test est créé par concept.
Notez que chaque méthode qui teste une fonctionnalité commence par test_.

Par exemple, le premier test garantit qu’un mot écrit entièrement en minuscules est correctement transformé en un mot écrit entièrement en majuscules.
Le test lui-même consiste en un appel à la fonction assertEqual(expected, actual, msg=None).
Elle teste si expected et actual sont égaux, et échouera s’ils ne le sont pas.
De plus, elle garantit que expected et actual sont du même type.

Exécutez cette classe de tests pour confirmer que les concepts ont été correctement implémentés,
indiquant tout bug via des tests échoués.

En exécutant unittest.main(), toutes les méthodes de la classe TestInverseCapitalization qui commencent par test_ sont exécutées.
Les résultats sont affichés dans le terminal, montrant quels tests ont réussi et lesquels ont échoué.
C’est plus intéressant que d’avoir simplement une liste d’assertions, car la bibliothèque génère un rapport complet, ce qui est meilleur pour le débogage.

Les frameworks de test fournissent un ensemble de méthodes prêtes à l’emploi qui simplifient l’élaboration des tests : unittest assert methods et JUnit assertions.

2.2 — Écrire le code ou les tests en premier ?

Une bonne pratique est d’avoir les tests écrits avant que le code ne soit écrit par quiconque autre que la personne qui développera le code :

  • Les tests écrits à l’avance servent de spécification claire du comportement attendu.
  • La personne écrivant les tests se concentre sur ce que le code doit accomplir sans être influencée par les détails d’implémentation.
  • Cela favorise une approche de développement axée sur la fonctionnalité et les besoins des utilisateurs.
  • Cela encourage la communication et garantit que les deux parties ont une compréhension partagée des objectifs du projet.

Pour aller plus loin

Lorsque le code que vous souhaitez tester interagit avec d’autres composants de votre code, il est possible de simuler la partie du code avec laquelle vous interagissez.
Cela s’effectue en utilisant la bibliothèque mock object.
C’est particulièrement pertinent lorsqu’une fonction attend une entrée utilisateur.

Pour aller encore plus loin