Les logiciels embarqués restent généralement inchangés après le lancement du produit qu'ils équipent. Alors comment pouvez-vous être sûr que votre code est exempt de bogues ? Nous nous plongeons ici dans le monde des tests logiciels pour le découvrir !

Dans « The Art of Software Testing » de Glenford J. Myers, l'auteur commence par demander au lecteur de réfléchir à la manière de tester un programme exemple. Si vous n'avez jamais écrit de test logiciel, vous réalisez soudain à quel point les tests sont difficiles. Par où commencer ? Que couvrir ? Quand terminer ? Mais la question la plus cruciale est peut-être : qu'est-ce qu'un test logiciel ?

Myers nous fournit une excellente définition pour répondre à cette dernière question :

« Le test est le processus d'exécution d'un programme avec l'intention de trouver les erreurs. »

Les logiciels ont souvent mauvaise réputation. Contrairement au matériel, tel que la conception d'une carte de circuit imprimé ou d'une puce, qui reste fixe pendant toute la durée de vie du produit, le logiciel peut être modifié. On pense donc qu'il n'est pas nécessaire d'être aussi strict en ce qui concerne son développement, car toute erreur peut être corrigée assez facilement. Si cela est vrai pour une machine connectée à Internet dans un bureau ou un centre de données, ce n'est pas le cas si vous concevez des satellites dont la durée de vie est de 20 ans.

Le développement de logiciels embarqués ne s'inscrit pas dans cette mentalité de « nous pouvons le réparer plus tard », car le logiciel est lié au matériel, en particulier le code qui est proche des périphériques. De plus, la plupart des microcontrôleurs ne sont pas connectés à Internet et, s'ils le sont, comme un capteur IdO, la bande passante disponible n'est probablement pas suffisante pour supporter le téléchargement d'une nouvelle image de micrologiciel.

Il existe également un autre défi. Si vous développez du code sur un PC, vous pouvez exécuter des tests relativement rapidement et voir les résultats sur votre écran. Sur un microcontrôleur, vous n'avez probablement pas d'écran. Ou un clavier, d'ailleurs. Alors comment les développeurs testent-ils les logiciels embarqués ?

    Avez-vous déjà effectué des tests ?


    Test des logiciels embarqués par rapport aux exigences

    La première étape pour tester avec succès un logiciel embarqué est une bonne définition de ce que le logiciel doit faire. Il faut rédiger un document d'exigences qui explique la fonctionnalité attendue et les limitations éventuelles. Par exemple, si vous avez une fonction qui convertit les mesures de température de Celsius en Fahrenheit, une exigence serait que la conversion soit précise pour toutes les valeurs. La conversion mathématique, pour rappel, est la suivante :
     
    TempF = TempC × (9/5) + 32
     
    Cependant, le développement de code pour les systèmes embarqués exige souvent que nous opérions sous les contraintes des ressources limitées des petits microcontrôleurs. Ainsi, sans la prise en charge de la virgule flottante, cela peut signifier qu'il faut développer une fonction en utilisant uniquement des calculs en nombres entiers. Cela entraîne inévitablement des niveaux d'erreur plus élevés dans le résultat que ce que l'on attendrait d'une implémentation en virgule flottante. Cela serait donc déclaré dans les exigences, probablement comme le niveau de précision que l'on peut attendre.

    Les mathématiques des nombres entiers peuvent également limiter la plage des valeurs pouvant être converties. Un caractère signé de huit bits limite la plage d'entrée de -128 à +127, mais limite également le résultat à la même plage. Cela restreint implicitement la plage d'entrée de -88° C à +52° C, qui se convertit en -127° F à +127° F. Bien que tout cela semble très restrictif, il s'agit d'une restriction tout à fait réaliste pour un microcontrôleur huit bits destiné à mesurer la température dans une maison. Une comparaison du code utilisant les implémentations char et float de huit bits est fournie à la fin de cet article.
     
    Il est également essentiel de prendre en compte les tests dont vous savez qu'ils doivent échouer. Par exemple, une conversion de -100° C ne devrait pas donner -148° F pour notre implémentation de fonction optimisée. Si c'est le cas, il y a peut-être un problème. Ce genre de situation peut se produire lorsque le caractère n'est pas limité à huit bits sur certaines architectures de processeur.

    Ces limites en termes de fonctionnalité et de gamme d'entrées/sorties nous aideront également à définir nos tests.

    Tests en boîte noire ou en boîte blanche

    Une fois la spécification en main, il existe deux approches fondamentales pour développer les tests : ceux en boîte noire et en boîte blanche. La boîte noire suppose que vous testez le code selon la spécification sans connaître l'implémentation. Les tests en boîte blanche prennent en compte les spécifications pendant le développement des tests, mais avec une compréhension du fonctionnement du code.

    Dans le cas de notre fonction de conversion de température, les tests en boîte noire pourraient donner lieu à une collection exhaustive de tests. Toutes les entrées valides possibles pourraient être testées comme défini dans la spécification (-88 à +52° C). Le résultat, en Fahrenheit, serait vérifié selon la précision spécifiée (+/- 1° F). Dans cet exemple, le nombre de tests qui en résulte est important, mais gérable. Cependant, si nous devions prendre en charge des valeurs de 16 bits, ou avoir plusieurs valeurs d'entrée, le nombre de cas de test qui en résulte deviendrait rapidement ingérable, tant du point de vue de la quantité que du temps d'exécution du texte.
     
    Les tests en boîte noire développent des tests basés uniquement sur les exigences. Les tests en boîte blanche utilisent en plus une compréhension de l'implémentation du code.

    Tests en boîte noire

    Pour rendre les tests plus faciles à gérer, certaines hypothèses sur le code à tester peuvent être faites pour réduire le nombre de tests. Par exemple, si la fonction convertit correctement 25° C, elle fonctionne probablement aussi correctement pour 26 et 24. Ceci est connu comme une classe d'équivalence et est utilisé pour réduire formellement le nombre de cas de test sans nuire à leur qualité.

    Il existe d'autres stratégies pour réduire le nombre de cas de test. L'analyse des valeurs limites examine les conditions limites des classes d'équivalence. Dans notre exemple, nous examinerions les limites des valeurs d'entrée telles que définies par la spécification (par exemple, -88 à -86° C, et +50 à +52° C). En tant que programmeur, nous savons également que des problèmes peuvent survenir lorsque des variables sont définies de manière incorrecte, par exemple unsigned char au lieu de char. Par conséquent, les tests pour des entrées de -1° C, 0° C et 1° C sont judicieux, tout comme les tests qui attendent des résultats de -1° F, 0° F et 1° F.

    La gamme complète des approches de test de boîte noire, telles qu'elles sont énumérées par Myers, est la suivante :
    • Partitionnement par équivalence.
    • Analyse des valeurs limites.
    • Graphisme cause-effet : une méthode formelle de développement de tests adaptée aux systèmes complexes.
    • L'estimation des erreurs : une méthode informelle de développement de tests basée sur l'intuition et l'expérience.

    Tests en boîte blanche

    Les tests en boîte blanche adoptent une approche différente. Le développeur du test connaît l'implémentation du code. Dans notre exemple, la fonction ne contient qu'une seule ligne de code source C : l'équation mathématique permettant de convertir les degrés Celsius en degrés Fahrenheit. Ainsi, les tests qui en résulteraient ne seraient pas très différents de ceux créés en utilisant une approche boîte noire.

    Cependant, si le code source contient des décisions, telles que des instructions if et switch, les choses changent. En connaissant la logique du programme, un testeur peut créer des tests qui garantissent que tous les chemins possibles dans le code sont exercés. Ainsi, les cas de test passeront certaines combinaisons de valeurs apparemment étranges, de sorte que les lignes de code profondes dans le logiciel sont atteintes. Là encore, différentes approches permettent aux équipes d'atteindre des niveaux de couverture de test variables.

    Myers énumère les éléments suivants :
    • La couverture des instructions : s'assurer que toutes les déclarations sont exécutées.
    • Couverture des décisions : s'assurer que tous les énoncés de décision sont testés pour fournir vrai et faux au moins une fois.
    • Couverture des conditions : s'assurer que les conditions des instructions de décision sont testées pour fournir vrai et faux (comme if(A && B) ).
    • Couverture des décisions/conditions : les deux approches sont nécessaires dans le code avec un flux plus complexe afin d'exercer plus de chemins possibles.
    • Couverture des décisions multiples : généralement connue sous le nom de couverture condition/décision modifiée (MC/DC), cette approche approfondie couvre également les chemins que les alternatives ci-dessus peuvent cacher. Elle est utilisée pour tester les logiciels critiques pour la sécurité déployés dans certaines applications automobiles, médicales, aérospatiales et spatiales.
     
    Exemples de classes d'équivalence et analyse des valeurs limites pour notre fonction de conversion de température.
    Lors de vos recherches sur les tests de logiciels, vous pouvez également rencontrer des tests en boîte grise. Cette approche se situe entre les deux approches décrites ci-dessus, lorsque le développeur du test a un certain aperçu de l'implémentation du code, mais moins que ce qui est disponible pour le test en boîte blanche.

    Quand tester un logiciel embarqué

    Il n'est jamais trop tôt pour commencer les tests. Et même s'il est tentant d'écrire des tests pour son propre code, cette tâche doit être confiée à une personne qui ne connaît pas le logiciel. Les approches ci-dessus donnent lieu à de nombreux tests qui peuvent prendre des heures à exécuter. Cependant, vous voudrez vérifier que votre environnement de test fonctionne avant d’en lancer un de nuit. Il est donc utile de développer un scénario de test avec une poignée de tests qui peuvent le prouver. C'est ce qu'on appelle un test de simulation.

    La dénomination des tests dépend en gros du stade de développement de votre projet. Les tests de notre fonction de conversion de température sont dits unitaires. Ils testent une fonction ou un module de manière isolée. Un tel code peut être testé sur un PC, car il est universel et ne dépend pas des capacités du microcontrôleur cible. Les logiciels permettant de réaliser des tests unitaires comprennent Unity, conçu pour les développeurs embarqués, et CppUnit pour C++.

    Les tests sont généralement créés en utilisant une assertion, une déclaration du résultat correct attendu. Si le résultat est incorrect, l'échec du test est noté et signalé à la fin de tous les tests. Un exemple utilisant Unity est fourni ci-dessous :
     
    // Example tests developed using Unity

    // Variable value test
    int a = 1;
    TEST_ASSERT( a == 1 ); //this one will pass
    TEST_ASSERT( a == 2 ); //this one will fail

    // Example output for failed test:
    TestMyModule.c:15:test_One:FAIL

    // Function test; function is expected to return five
    TEST_ASSERT_EQUAL_INT( 5, FunctionUnderTest() );

    (Source: Unity)

    Le test des piles de protocoles est plus difficile, car celles-ci doivent partager des données avec les couches supérieures et inférieures. Pour y parvenir, des implémentations logicielles connues sous le nom de stub simulent le comportement attendu.

    Le code qui fonctionne directement sur les périphériques d'un microcontrôleur est plus difficile à tester. Une approche consiste à développer une configuration Hardware-in-the-Loop (HIL). Par exemple, lors du test d'un code qui initialise l'UART, un deuxième microcontrôleur pourrait être utilisé pour confirmer le bon fonctionnement de chaque test. Une autre solution consiste à utiliser un analyseur logique doté d'une interface de programmation pour capturer la sortie, vérifier la vitesse de transmission et la configuration correcte de la parité et des bits d'arrêt.

    Plus tard dans le processus de développement, les différents modules logiciels seront combinés. Par exemple, nous pouvons souhaiter sortir notre résultat Fahrenheit en utilisant un tampon circulaire lié à l'interface UART. Cette étape nécessite des tests d'intégration pour déterminer si les différents modules logiciels fonctionnent toujours correctement lorsqu'ils sont liés les uns aux autres. Pour les systèmes embarqués, cela nécessitera également une approche HIL.

    Pour un produit achevé, il faut procéder à des tests de système. À ce stade, il n'est pas vraiment nécessaire d'examiner la fonctionnalité du code. L'équipe de test se concentre plutôt sur la fonctionnalité globale du système. Elle examinera si la pression d'un bouton entraîne la bonne réponse, si l'écran affiche les bons messages et si la fonctionnalité résultante est conforme aux attentes.

    Test de logiciels embarqués, pas aussi facile qu'il n'y paraît

    Les tests sont un sujet complexe, avec des défis allant du choix des tests à mettre en œuvre à la façon de les exécuter. Heureusement, il existe de nombreuses ressources de qualité qui expliquent les approches de test formelles. Même le livre de Myers, écrit dans les années 1970, est toujours d'actualité. Le développement des tests est également facilité par une série de cadres à code source ouvert, ce qui réduit la barrière à l'entrée pour les développeurs de systèmes embarqués qui souhaitent améliorer leur approche.

    Exemple de code pour la conversion de Celsius en Fahrenheit
     

    Les microcontrôleurs à huit bits fonctionnent plus efficacement avec des valeurs à huit bits. En outre, il est peu probable qu'ils intègrent du matériel permettant d'accélérer les calculs en virgule flottante. Il est donc logique d'écrire des fonctions optimisées pour le processeur, si possible rapides et efficaces. En limitant la plage des valeurs Celsius prises en charge, la fonction de conversion de température suivante, basée sur des caractères, est cinq fois plus rapide que la même fonction utilisant des variables flottantes. Les temps d'exécution sont basés sur un Arduino MEGA (ATmega2560) compilé avec les paramètres standard de l'EDI Arduino.
     
    // 8-bit Celsius to Fahrenheit conversion function. 
    // Required 5.5µs to execute
    // Limited to Celsius range of -88°C to +52°C
    char convertCtoF(char celsius) {
      int fahrenheit = 0;

      digitalWrite(7, HIGH);
      // F = (C * (9/5)) + 32 - convert celsius to fahrenheit
      // (9/5) = 1.8
      fahrenheit = ((18 * ((int) celsius)) + 320) / 10;
      digitalWrite(7, LOW);
      
      return (char) fahrenheit;
    }

    // Floating-point Celsius to Fahrenheit conversion function. 
    // Required 24.3µs to execute
    // Limited to range of 'float'
    float fpConvertCtoF(float celsius) {
      float fahrenheit = 0.0;

      digitalWrite(7, HIGH);
      fahrenheit = (celsius * (9.0 / 5.0)) + 32.0;
      digitalWrite(7, LOW);
      
      return fahrenheit;
    }