Linux Embedded

Le blog des technologies libres et embarquées

Présentation et intégration de Robot Framework dans le milieu de l'embarqué

Dans cet article, je vous propose de découvrir les bases de Robot Framework et son intégration dans le milieu de l’embarqué. Je présenterai quelques exemples de tests et de mocks basés sur mon expérience professionnelle, l’article se veut pédagogique, mais il s’adresse à des personnes ayant déjà des connaissances de base en C++/Python 3 et en tests logiciels.

L’historique

Robot Framework est un framework de tests génériques automatisés écrit en Python. Il a été développé en 2005 chez Nokia Networks, puis publié en open source avec la version 2.0 en 2008 sous licence Apache 2.0. La dernière version (7.3.2) est sortie le 4 juillet 2025.

Il y a de nombreuses bibliothèques open source Robot Framework disponibles développées par la communauté ou par des entreprises, pour supporter différents protocoles, comme Requests pour les API, DatabaseLibrary pour les bases de données etc…

Pourquoi utiliser Robot Framework ? Et surtout dans l’embarqué ?

Pourquoi utiliser Robot Framework pour automatiser vos tests ? Il permet de faire des tests d’intégration de haut niveau, de bout en bout et avec des mocks Python pour simuler des devices, des communications CAN, des appels d’API etc…

Vous pouvez, par exemple, exécuter différents types de tests : tester chaque projet individuellement avec des mocks, puis tester l’ensemble des projets réunis afin de vérifier le bon fonctionnement global de vos applications (tests d’intégration et de bout en bout), comme le backend, le frontend/API et l’application embarquée, à travers plusieurs scénarios de test.

Avantages

  • Open source et gratuit, avec une grande communauté active
  • Framework de tests générique, mature et stable
  • Permet des tests de haut niveau, d’intégration et de bout en bout.
  • Nombreuses bibliothèques disponibles pour Robot Framework et Python (CAN, HTTP, MQTT, AMQP, Web, Base de données, etc…)
  • Simple à apprendre et à utiliser, même pour des personnes moins techniques
  • Exportation des résultats au format xUnit, utile avec GitLab CI, Jenkins, etc
  • Génère des rapports et des logs détaillés en HTML

Inconvénients

  • Syntaxe spécifique, différente des langages de programmation classiques
  • Peut devenir verbeux pour des scénarios complexes
  • Peut être redondant avec des tests unitaires déjà présents en C/C++
  • Dépendance à Python et aux bibliothèques tierces
  • Bien plus lent que des tests unitaires natifs

Fonctionnement de Robot Framework

Les fichiers de Robot Framework ont l’extension .robot, c’est ce que l’on appelle une suite de tests, ils sont structurés en sections: *** Settings ***, *** Variables *** etc… Chacune de ces sections contiennent des mots clés pour configurer, inclure, exécuter et documenter les tests.

Lors de l’execution des suites de tests, il va générer plusieurs rapports HTML détaillés avec les résultats de chaque test et de chaque suite de tests, en option d’autres formats comme XML, xUnit etc…

Un fichier .robot se présente sous cette forme, c’est ce que l’on appelle une suite de test, mais nous allons détailler chaque section plus loin dans l’article:

*** Settings ***
Resource        MyBeautifulMock.robot
Library         OperatingSystem
Suite Setup      Prepare Suite
Suite Teardown   TearDown Suite
*** Variables ***
${MY_GLOBAL_VARIABLE}    Hello, World!
*** Test Cases ***
Basic Test Case 1
    [Documentation]    This is the first test case
    [Tags]    basic_test
    Log    This is the first test case: ${MY_GLOBAL_VARIABLE}
    Should Be True     ${True}
Basic Test Case 2
    [Documentation]    This is the second test case
    [Tags]    basic_test
    Log    This is the second test case: ${MY_GLOBAL_VARIABLE}
    Should Be True     ${True}
*** Keywords ***
Prepare Suite
    Log    Preparing the test suite
TearDown Suite
    Log    Tearing down the test suite

La syntaxe

Comme avec Python, l’indentation est très importante dans Robot Framework, elle permet de définir les blocs de code et la hiérarchie des éléments, les espaces sont utilisés pour séparer les arguments des keywords ou les éléments d’une liste etc… Je vous recommande d’utiliser 4 espaces pour l’indentation, comme en Python.

Pour écrire une variable, il faut utiliser le format ${VARIABLE_NAME} ou @{MY_LIST}, les variables ne sont pas typées (comme en python), elles peuvent contenir des strings, des nombres, des listes ou des dictionnaires:

${MY_STRING}    Hello, World!
${MY_NUMBER}    42
${MY_LIST}    [item1, item2, item3]
@{MY_LIST_ELEMENT}      item1    item2    item3
&{MY_DICT}      key1=value1    key2=value2

 

Il est important de bien comprendre la différence entre @{} et ${} lors de la manipulation des listes:

  • @{}: Chaque élément de la liste est passé comme un argument distinct: item1    item2    item3

  • ${}: L’objet liste est passé comme un seul argument, sous forme d’un bloc contenant tous les éléments: [item1, item2, item3]

Cette distinction est importante lorsqu’on appelle des keywords :

  • Si un keyword attend plusieurs arguments séparés, on utilisera @{}.

  • Si un keyword attend une liste en un seul argument, on utilisera ${}.

 

Pour l’appel d’un keyword, il faut utiliser des espaces pour séparer les arguments:

Log    This is a log message.
Should Be Equal    ${MY_NUMBER}    42

Vous pouvez faire des boucles avec FOR et END, voici un exemple:

@{MY_LIST}    item1    item2    item3
FOR    ${item}    IN    @{MY_LIST}
    Log    Item: ${item}
END

 

Les keywords génériques

Les keywords sont des actions qui peuvent être utilisées dans les cas de test, ils peuvent être définis dans le fichier de test ou importés depuis des bibliothèques externes, ils sont définit en Camel Case avec des espaces entre les mots et peuvent prendre des arguments, ils sont équivalents aux fonctions dans les autres langages de programmation.

Il a y de très nombreux keywords prédéfinis dans Robot Framework, voici les principaux que j’ai utilisés dans mes différents projets:

Log/Log To Console: Affiche un message dans le rapport de test ou dans la console:

Log    Hello, Robot Framework!

Should Be True: Vérifie si une condition retourne True:

Should Be True    ${True}

Should Be Equal/Should Not Be Equal: Vérifie si deux valeurs sont égales ou différentes:

Should Be Equal    ${42}    ${42}

Wait Until Keyword Succeed: Execute le keyword Check CAN Message jusqu’à ce qu’il réussisse toutes les 1 secondes, lorsque le timeout de 5 secondes est dépassé, le test échoue:

Wait Until Keyword Succeeds    5 seconds    1 second    Check CAN Message    ${id}

Set Global Variable: Définit une variable globale disponible dans tous les autres tests:

Set Global Variable    ${MY_GLOBAL_VARIABLE}    Hello, World!

Run Keyword If: Exécute un keyword si une condition est vraie:

Run Keyword If    '${MY_GLOBAL_VARIABLE}' == 'Hello, World!'    Log    The global variable is set correctly.

Run Keyword And Ignore Error: Exécute un keyword et ignore les erreurs:

Run Keyword And Ignore Error    Should Be True    ${False}

Sleep: Met en pause l’exécution pour une certaine durée, le format peut être en millisecondes, secondes, minutes:

Sleep    5 seconds
Sleep    1.5s  # 1.5 secondes
Sleep    2m 1s 120ms  # 2 minutes, 1 seconde et 120 millisecondes

Set Tags: Change les tags dynamiquement pour la suite de tests:

Set Tags    my_tag    another_tag    test_type_33

Pass Execution If/Skip If: Passe ou ignore un test si une condition est remplie:

Skip If    '${MY_GLOBAL_VARIABLE}' != 'Hello, World!'    The global variable is not set correctly.
Pass Execution If    '${MY_GLOBAL_VARIABLE}' == 'Hello, World!'    The global variable is set correctly.

 

Structure des fichiers de test

Les tests sont organisés en suites de tests, qui contiennent des cas de test et chaque cas de test est composé de keywords qui représentent des actions ou des assertions.

Section Settings

La section *** Settings *** définit les bibliothèques, les ressources utilisées dans le fichier de test.

*** Settings ***
# Permet d'inclure des bibliothèques externes Robot Framework, comme des keywords ou des variables définis dans un autre fichier `.robot`.
Resource        MyBeautifulMock.robot
# Permet d'inclure des bibliothèques externes Python, comme des bibliothèques et classes ou des mocks.
Library         OperatingSystem
Library         MyCustomClass.py
# Définit un keyword qui s'exécute une fois avant chaque suite de tests.
Suite Setup      Prepare Suite
# Définit un keyword qui s'exécute une fois après chaque suite de tests.
Suite Teardown   TearDown Suite
# Définit un keyword qui s'exécute avant chaque test unitaire.
Test Setup       My Test Preparation
# Définit un keyword qui s'exécute après chaque test unitaire.
Test Teardown    My Test Cleanup
# Documentation pour la suite de tests, elle apparaîtra dans le rapport généré.
Documentation     This is a test file for demonstrating Robot Framework features.
# Tags par défaut pour tous les tests dans ce fichier, ils peuvent être surchargés dans chaque test.
Test Tags          test tag 1    test tag 2    test tag 3

Section Keywords

La section *** Keywords *** définit les keywords personnalisés qui peuvent être réutilisés dans les cas de test, équivalent à des fonctions, ici un keyword Start Web Service qui prend un argument obligatoire ${param1} et un argument optionnel ${param2} avec une valeur par défaut None:

Start Web Service
    [Documentation]    Start the service with the given parameters.
    [Arguments]    ${param1}    ${param2}=None
    Log    Starting service with ${param1} and ${param2}
Stop Web Service
    [Documentation]    Stop the service.
    Log    Stopping service.

Les arguments avec valeur par défaut doivent toujours être placés après les arguments obligatoires, pour un argument variatique, vous pouvez utiliser @{args} et doit etre placé en dernier.

Section Variables

La section *** Variables *** définit les variables globales disponibles dans les tests.

${EXEC_NAME}         myexec
${TEXT}      Hello
..        world!

Pour définir une liste et un dictionnaire:

@{MY_LIST}    item1    item2    item3
&{MY_DICT}    key1=value1    key2=value2

Section Test Cases

La section *** Test Cases *** définit les cas de test, qui sont des scénarios à exécuter, elle est unique aux suites de tests.

My First Test Case
    [Documentation]    This is a simple test case.
    [Tags]    my_tag    another_tag    test_type_33
    Log    This is the first test case.
    Should Be True     ${True}
My Second Test Case
    Log    This is the second test case.
    Should Be True     ${True}

Il est possible d’override [Teardown] et [Setup] dans un cas de test pour faire des préparations ou nettoyages spécifiques:

My First Test Case
    [Documentation]    This is a simple test case.
    [Setup]    My Custom Test Preparation    ${arg1}    ${arg2}
    Should Be True     ${True}
    [Teardown]    My Test Cleanup

 

Les bibliothèques

Robot Framework dispose de plusieurs bibliothèques intégrées:

  • BuiltIn : contient des keywords de base pour les assertions et d’autres fonctionnalités essentielles, elle est importée par défaut.
  • String : permet de manipuler les chaînes de caractères.
  • Collections : pour travailler avec les listes et les dictionnaires.
  • DateTime : pour manipuler les dates et les heures.
  • OperatingSystem : pour gérer les fichiers et les répertoires.
  • Process : pour exécuter des processus, comme des commandes ou des exécutables.
  • Dialogs : met en pause l’exécution des tests et demande une action à l’utilisateur.
  • XML : pour manipuler les fichiers XML.

Il existe également de nombreuses bibliothèques open source communautaires ou maintenues par des entreprises:

  • SeleniumLibrary : pour automatiser les tests d’applications web.
  • RequestsLibrary : pour tester les API.
  • DatabaseLibrary : pour tester les bases de données.
  • SSHLibrary : pour exécuter des commandes sur des machines distantes via SSH.

Vous trouverez une liste complète des bibliothèques sur MarketSquare.

Bien entendu, toutes les bibliothèques Python peuvent être utilisées dans Robot Framework, voici quelques exemples des bibliothèques intégrées les plus couramment utilisées:

  • python-can : afin d’envoyer et recevoir des messages CAN.
  • pyserial : dans le but de communiquer avec des périphériques série.
  • Flask : Serveur web léger pour simuler des API.
  • requests : pour faire des requêtes HTTP.
  • pymysql : Communications avec des bases de données MySQL.
  • pymavlink : pour établir une communication via le protocole MAVLink.
  • socket : Communications réseau bas niveau (TCP/UDP).

 

Process 

La bibliothèque Process permet d’exécuter des commandes système et de gérer les processus, voici quelques exemples:

Start Process: Démarre un processus en arrière-plan:

${process}=    Start Process    my_command   --option value

Get Process Result: Récupère le résultat d’un processus démarré avec Start Process:

${result}=    Get Process Result    ${process}

Is Process Running: Vérifie si un processus est toujours en cours d’exécution:

${is_running}=    Is Process Running    ${process}

Run Process: Exécute une commande et attend qu’elle se termine:

${result}=    Run Process    my_command   --option value

 

OperatingSystem 

La bibliothèque OperatingSystem permet de réaliser des opérations sur les fichiers et les répertoires, voici quelques exemples:

Create File/Create Directory: Crée un fichier ou un répertoire:

Create Directory    my_directory
Create File    my_directory/my_file.txt    This is first line.

File Should Exist/Directory Should Exist: Vérifie si un fichier ou un répertoire existe:

File Should Exist    my_directory/my_file.txt
Directory Should Exist    my_directory

Append To File: Ajoute du contenu à un fichier:

Append To File    my_directory/my_file.txt    This is a second line.

Copy File: Copie un fichier:

Copy File    my_directory/my_file.txt    my_directory/my_file_copy.txt

List Directory: Liste les fichiers et répertoires dans un répertoire:

${files}=    List Directory    my_directory

Get File: Lit le contenu d’un fichier et le retourne:

${content}=    Get File    my_directory/my_file.txt

Remove File/Remove Directory: Supprime un fichier ou un répertoire:

Remove File    my_directory/my_file.txt
Remove Directory    my_directory

 

String

La bibliothèque String permet de manipuler les chaînes de caractères, voici quelques exemples:

Convert To Upper Case/Convert To Lower Case: Convertit une chaîne de caractères en majuscules ou en minuscules:

${upper}=    Convert To Upper Case    hello
${lower}=    Convert To Lower Case    HELLO

Should Be Not Empty: Vérifie si une chaîne de caractères n’est pas vide:

Should Be Not Empty    ${MY_STRING}

Get Regexp Matches : Récupère les correspondances d’une expression régulière dans une chaîne de caractères:

${matches}=    Get Regexp Matches    My beautiful code    \b\w{3}\b

 

Collections

La bibliothèque Collections permet de manipuler les listes et les dictionnaires, voici quelques exemples:

Append To List/Remove From List: Ajoute ou supprime un élément à une liste:

Append To List    ${MY_LIST}    item4
Remove From List    ${MY_LIST}    item2

Get From List: Récupère un élément d’une liste par son index:

${item}=    Get From List    ${MY_LIST}    0

Should Contain: Vérifie si une liste contient un élément:

Should Contain    ${MY_LIST}    item1

Should Not Be Empty: Vérifie si une liste n’est pas vide:

Should Not Be Empty    ${MY_LIST}

Nous avons abordé les keywords prédéfinis les plus couramment utilisés, je vous invite à consulter la documentation officielle pour plus de détails.

Vous pouvez également créer vos propres keywords pour réaliser des actions spécifiques, voici quelques exemples:

Log Can Message
    [Arguments]    ${data}
    Log    Sending Init CAN Message: ${data}
Send CAN Init Message
    [Arguments]    ${data}
    Log Can Message    ${data}
    Send CAN Message    ${42}    ${data}

 

Les mocks Python

Robot Framework permet d’utiliser des bibliothèques Python, vous pouvez donc créer des mocks pour simuler des devices, des communications CAN, des appels d’API etc…

Voici un exemple tronqué de mock Python pour simuler une API avec Flask:

from robot.libraries.BuiltIn import BuiltIn
from robot.api.deco import keyword, library
# Le scope de la bibliothèque peut etre:
# TEST = elle est instanciée une fois par test
# SUITE = elle est instanciée une fois par suite de tests
# GLOBAL = elle est instanciée une fois pour toute l'exécution des suites de tests
# auto_keywords=False = les fonctions doivent etre définis avec le décorateur @keyword
@library(scope="SUITE", version='1.2', auto_keywords=False)
class WebServer:
    def __init__(self):
        self.built_in = BuiltIn()
        self.built_in.log("WebServer library initialized")
# Permet de logger un message dans le rapport de test depuis le mock Python
# La fonction start_web_server sera disponible dans Robot Framework
# avec le keyword "Start Web Server"
    @keyword("Start Web Server")
    def start_web_server(self):
        self.built_in.log("Starting web server...")
        self.start_flask_app()

 

Commandes de base

Nous allons voir les différentes commandes et options disponibles avec Robot Framework pour exécuter des tests, configurer l’environnement et générer les rapports de l’exécution des tests, Robot Framework se lance en CLI avec la commande robot, suivi des options et enfin du chemin vers les fichiers de tests.

Pour lancer les tests qui se trouvent dans le dossier tests/:

robot tests/

Randomiser les suites de tests et/ou les tests, utile pour éviter les dépendances entre les tests et s’assurer qu’ils peuvent être exécutés dans n’importe quel ordre:

robot --randomize all tests/
robot --randomize tests tests/
robot --randomize suite tests/

Exclure les tests avec le tag can_test:

Pour exécuter exclure ou inclure des tests avec des tags spécifiques:

robot --include api_test --exclude can_test tests/

Ignorer les tests avec le tag basic_test si ils échouent:

robot --skiponfailure basic_test tests/

Pour modifier le dossier de sortie et les fichiers de log et de rapport, vous pouvez utiliser les options suivantes:

robot --outputdir results --log my_test_log.html --report my_test_report.html --output my_test_output.xml tests/

Vous pouvez également sortir les résultats au format xUnit, utile pour l’intégration avec des outils CI/CD:

robot --xunit my_test_output.xml tests/

Pour du debugging, vous pouvez générer un fichier de debug:

robot --debugfile debug.txt tests/

Pour modifier les variables globales depuis la ligne de commande, vous pouvez utiliser l’option --variable:

robot --variable HELLO_ENDPOINT:"Hello, World" tests/

Robot Framework permet aussi d’ajouter un listener pour monitorer l’exécution des tests, voici un exemple basique de listener pour RF:

Fichier tests/RobotLogListener.py:

from robot.libraries.BuiltIn import BuiltIn
class RobotListenerV3:
    ROBOT_LIBRARY_SCOPE = "GLOBAL"
    ROBOT_LISTENER_API_VERSION = 3
    def __init__(self):
        pass
    def start_suite(self, suite, result):
        BuiltIn().log(f"Starting suite: {suite.name}")
        pass
    def start_test(self, test, result):
        BuiltIn().log(f"Starting test: {test.name}")
        pass
    def end_test(self, test, result):
        BuiltIn().log(f"Ending test: {test.name}")
        pass
    def end_suite(self, suite, result):
        BuiltIn().log(f"Ending suite: {suite.name}")
        pass

Pour lancer les tests avec le listener:

robot --listener tests/RobotLogListener.py tests/

Vous trouverez plus d’options dans la RobotListener.

Exemples de tests avec un projet C/C++

Nous allons voir des exemples concrets de l’utilisation de Robot Framework pour tester des projets C/C++ embarqués, nous allons utiliser des mocks Python pour simuler des communications CAN, ainsi qu’un serveur Flask pour simuler des appels d’API.

Pour ces exemples, nous avons 6 fichiers:

  • tests/testCAN.robot : Les tests pour vérifier l’envoi et la réception de messages CAN avec un mock Python.
  • tests/testAPI.robot : Les tests pour vérifier les appels d’API avec un serveur Flask.
  • tests/CanMock.py : Un mock pour simuler les communications CAN.
  • tests/WebServer.py : Un mock pour simuler une API avec Flask.
  • tests/Executable.robot : Une bibliothèque Robot Framework de tests pour exécuter des programmes.
  • tests/canProgram.cpp : Un programme C++ pour tester les communications CAN avec un écho et inversement des bits.

Fichier tests/CanMock.py:

#!/usr/bin/env python
import can
from robot.libraries.BuiltIn import BuiltIn
from robot.api.deco import keyword, library
@library(scope='SUITE', auto_keywords=False, version='1.2')
class CanMock:
    def __init__(self):
        self.channel = None
        self.interface = None
        self.bus = None
        can.rc['receive_own_messages'] = False
        can.rc['fd'] = True
        
        self.built_in = BuiltIn()
    @keyword('Mock Send CAN Message')
    def send_message(self, message_id : int, data : list):
        if not self.bus:
            raise Exception("CAN interface is not started. Please start the CAN interface before sending messages.")
        self.built_in.log(f"Sending CAN message with ID {message_id} and data {data}")
        msg = can.Message(arbitration_id=message_id, data=data, is_extended_id=False)
        self.bus.send(msg)
    @keyword('Mock Check Receive CAN Message')
    def check_receive_message(self, message_id : int, data : list):
        msg = self.bus.recv(1)
        if msg == None:
            raise Exception("No message received")
        if msg.arbitration_id != message_id:
            raise Exception(f"Received message ID {msg.arbitration_id} does not match expected ID {message_id}")
        if list(msg.data) != data:
            raise Exception(f"Received data {list(msg.data)} does not match expected data {data}")
        self.built_in.log(f"Received message ID {msg.arbitration_id} with data {list(msg.data)}")
    @keyword('Start CAN Mock')
    def start(self, channel="can1", interface="socketcan"):
        if self.bus:
            self.built_in.log("CAN interface is already started.")
            return
        self.channel = channel
        self.interface = interface
        self.bus = can.interface.Bus(channel=self.channel, interface=interface)
        self.built_in.log(f"CAN interface started on {self.interface}")
    @keyword('Stop CAN Mock')
    def close(self):
        self.bus.shutdown()
        self.bus = None

Fichier tests/WebServer.py:

#!/usr/bin/env python
from robot.libraries.BuiltIn import BuiltIn
from robot.api.deco import keyword, library
from flask import Flask, request
from multiprocessing import Process
from time import sleep
app = Flask(__name__)
@library(scope="SUITE", auto_keywords=False, version='1.1')
class WebServer:
    def __init__(self):
        self.app = self.app = Flask(__name__)
        self.built_in = BuiltIn()
        self.server_process = None
    @keyword("Start Web Server")
    def start_web_server(self):
        self.built_in.log("Starting web server...")
        self.server_process = Process(target=app.run, kwargs={"port": 5000, "host": "0.0.0.0", "debug": True})
        self.server_process.start()
        sleep(2)
    @keyword("Stop Web Server")
    def stop_web_server(self):
        if self.server_process:
            self.built_in.log("Stopping web server...")
            self.server_process.terminate()
            self.server_process.join()
            self.server_process = None
        sleep(2)
@app.route("/hello")
def hello():
    return "Hello, World!"
@app.route("/echo", methods=["POST"])
def echo():
    data = request.json
    return data, 200
@app.route("/route/<int:route_id>")
def route(route_id):
    return {"message": f"You have reached /route/{route_id}"}, 200

Fichier tests/canProgram.cpp:

#include <iostream>
#include <cerrno>
#include <cstring>
#include <chrono>
#include <thread>
#include <fcntl.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
auto main() -> int {
    std::string interface_name = "can1";
    std::cout << "Interface: " << interface_name << std::endl;
    int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (s < 0) {
        std::cerr << "Failed to open socket" << std::endl;
        return 1;
    }
    int enable = 1;
    if (setsockopt(s, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &enable, sizeof(enable)) < 0) {
        std::cerr << "Failed to enable CAN FD support" << std::endl;
        return 1;
    }
    int flags = fcntl(s, F_GETFL, 0);
    if (flags < 0) {
        std::perror("fcntl F_GETFL");
        close(s);
        return 1;
    }
    if (fcntl(s, F_SETFL, flags | O_NONBLOCK) < 0) {
        std::perror("fcntl F_SETFL O_NONBLOCK");
        close(s);
        return 1;
    }
    struct ifreq ifr;
    strcpy(ifr. ifr_name, interface_name.c_str());
    if (ioctl(s, SIOCGIFINDEX, &ifr) < 0) {
        std::cerr << "Failed to get interface index" << std::endl;
        return 1;
    }
    struct sockaddr_can addr;
    memset(&addr, 0, sizeof(addr));
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;
    if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0)
    {
        std::cerr << "Failed to bind socket" << std::endl;
        return 1;
    }
    ssize_t nbytes = 0;
    struct canfd_frame frameRead;
    struct canfd_frame frameTransmit;
    
    while (true) {
        std::this_thread::sleep_for(std::chrono::milliseconds(250));
        frameRead = {};
        frameTransmit = {};
        nbytes = read(s, &frameRead, CANFD_MTU);
        if (nbytes < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                continue;
            } else {
                std::perror("read");
                break;
            }
        }
        if (nbytes == CAN_MTU) {
            std::cout << "read: CAN 2.0 frame" << std::endl;
        } else if (nbytes == CANFD_MTU) {
            std::cout << "read: CAN FD frame" << std::endl;
        } else {
            std::cout << "read: other frame (wrong MTU)" << std::endl;
        }
        frameTransmit.can_id = frameRead.can_id;
        frameTransmit.len = 8;
        for (int i = 0; i < 8; i++) {
            frameTransmit.data[i] = ~frameRead.data[i];
        }
        if (write(s, &frameTransmit, CAN_MTU) != CAN_MTU) {
            std::cerr << "Failed to write CAN 2.0 frame" << std::endl;
        }
    }
    if (close(s) < 0) {
        perror("Close");
        return 1;
    }
    return 0;
}

 

Fichier tests/Executable.robot:

*** Settings ***
Library    Process
*** Keywords ***
Start Executable
    [Documentation]    Starts the executable process
    [Arguments]    ${EXECUTABLE}=./canProgram    ${EXEC_LOG}=canExec.log    @{ARGS}
    ${PROCESS_ID}=    Start Process    ${EXECUTABLE}    @{ARGS}    cwd=${CURDIR}    shell=True    stdout=${EXEC_LOG}    stderr=${EXEC_LOG}
    Sleep    1 seconds
    Is Process Running    ${PROCESS_ID}
    Log    Process ID: ${PROCESS_ID}
    RETURN    ${PROCESS_ID}
Stop Executable
    [Documentation]    Stops the executable process
    [Arguments]    ${PROCESS_ID}
    Terminate Process    ${PROCESS_ID}
    ${result}=    Get Process Result    ${PROCESS_ID}
    Terminate All Processes    kill=True
    RETURN    ${result}

Fichier tests/testAPI.robot:

*** Settings ***
Library           OperatingSystem
Library           Collections
Library           RequestsLibrary
Library        WebServer.py
Suite Setup      Prepare Suite
Suite Teardown   TearDown Suite
Documentation    Test API endpoints
*** Variables ***
${API_URL}             http://127.0.0.1:5000
${HELLO_ENDPOINT}     ${API_URL}/hello
${STATUS_ENDPOINT}    ${API_URL}/status
${ERROR_ENDPOINT}     ${API_URL}/error
${ECHO_ENDPOINT}      ${API_URL}/echo
@{GENERIC_ENDPOINTS}     ${API_URL}/route/1    ${API_URL}/route/2    ${API_URL}/route/3
*** Test Cases ***
Test Hello World From API
    [Documentation]    Test hello world endpoint
    [Tags]    api_test
    ${results}=    GET    ${HELLO_ENDPOINT}
    Should Be Equal As Strings    ${results.text}    Hello, World!
    Should Be Equal As Integers    ${results.status_code}    200
Test Echo From API
    [Documentation]    Test echo endpoint
    [Tags]    api_test
    ${input_data}=    Create Dictionary    message=Hello, Echo!
    ${results}=    POST    ${ECHO_ENDPOINT}    json=${input_data}
    Should Be Equal As Strings    ${results.json()['message']}    Hello, Echo!
    Should Be Equal As Integers    ${results.status_code}    200
Test Generic Endpoints From API
    [Documentation]    Test multiple endpoints
    [Tags]    api_test
    FOR    ${endpoint}    IN    @{GENERIC_ENDPOINTS}
        ${results}=    GET    ${endpoint}
        Should Not Be Empty    ${results.json()['message']}
        Should Be Equal As Integers    ${results.status_code}    200
    END
*** Keywords ***
Prepare Suite
    Log    Preparing the test suite
    Start Web Server
    Wait Can Ping Url    ${HELLO_ENDPOINT}
TearDown Suite
    Log    Tearing down the test suite
    Stop Web Server
Check Can Ping Url
    [Arguments]    ${url}
    ${response}=    GET    ${url}
    Run Keyword If    ${response.status_code} != 200    Fail    Request failed with code ${response.status_code}
    RETURN    ${response}
Wait Can Ping Url
    [Arguments]    ${url}    ${timeout}=20 sec    ${interval}=0.5 sec
    Log    Waiting for ${url} to be reachable...
    Wait Until Keyword Succeeds    ${timeout}    ${interval}    Check Can Ping Url    ${url}

Fichier tests/testCAN.robot:

*** Settings ***
Resource        Executable.robot
Library           CanMock.py
Suite Setup      Prepare Suite
Suite Teardown   TearDown Suite
Documentation    Test canProgram who echo CAN messages with revert data bits
*** Variables ***
${process_id_exec}    None
*** Test Cases ***
Test CAN 1
    [Documentation]    Basic CAN test case 1
    [Tags]    can_test
    Mock Send CAN Message    message_id=0x123    data=[0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF]
    Mock Wait CAN Message    message_id=0x123    data=[0xED, 0xCB, 0xA9, 0x87, 0x6F, 0x54, 0x32, 0x10]    timeout=5 seconds
Test CAN 2
    [Documentation]    Basic CAN test case 2
    [Tags]    can_test
    Mock Send CAN Message    message_id=0x321    data=[0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10]
    Mock Wait CAN Message    message_id=0x321    data=[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]    timeout=5 seconds
Test CAN 3
    [Documentation]    Falling CAN with wrong message id
    [Tags]    can_test
    Mock Send CAN Message    message_id=0x456    data=[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]
    Mock Wait CAN Message    message_id=0xFFFF    data=[0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11]    timeout=2 seconds
*** Keywords ***
Prepare Suite
    Log    Preparing the test suite
    Start CAN Mock    channel=can1
    ${process_id_exec}=    Start Executable    EXECUTABLE=./canProgram    EXEC_LOG=canExec.log
    Log    Executable started with process ID: ${process_id_exec}
    Set Global Variable    ${process_id_exec}
    
TearDown Suite
    Log    Tearing down the test suite
    Stop CAN Mock
    Stop Executable    ${process_id_exec}
Mock Wait CAN Message
    [Documentation]    Waits for a CAN message to be received
    [Arguments]      ${message_id}    ${data}    ${timeout}=10 seconds
    Log    Waiting for CAN message with ID ${message_id} and data ${data} within ${timeout}
    Wait Until Keyword Succeeds    ${timeout}    1 seconds    Mock Check Receive CAN Message     message_id=${message_id}    data=${data}

Le dossier devrait ressembler à ceci :

tree .
.
└── tests
    ├── CanMock.py
    ├── canProgram.cpp
    ├── Executable.robot
    ├── testAPI.robot
    ├── testCAN.robot
    └── WebServer.py
1 directories, 6 files

Installation de l’environnement Python et Robot Framework:

python3 -m venv myvenv && source myvenv/bin/activate && pip install robotframework==7.3.2 robotframework-requests==0.9.7 python-can==4.6.1 flask==3.1.2

Pour la toolchain GCC (Debian/Ubuntu):

sudo apt install build-essential can-utils

Pour ArchLinux/Manjaro:

sudo pacman -S base-devel && yay -S can-utils

Pour Fedora:

sudo dnf groupinstall "Development Tools" && sudo dnf install can-utils

Vous pouvez maintenant créer le bus CAN virtuel can1, il nous sera utile pour les tests CAN:

sudo ip link add can1 type vcan && sudo ip link set can1 up mtu 72

Avant de lancer les tests, vous devez build le programme CAN C/C++:

g++ tests/canProgram.cpp -O2 -std=c++17 -o tests/canProgram

Dans un autre terminal, vous pouvez utiliser candump pour écouter les messages CAN sur le bus virtuel lors de l’exécution des tests:

candump -cexdtA any

Vous pouvez maintenant lancer les tests avec Robot Framework:

robot tests/

Vous devriez voir une sortie similaire à celle-ci avec plusieurs fichiers générés et un test échoué:

==============================================================================
Tests                                                                         
==============================================================================
Tests.testAPI :: Test API endpoints                                           
==============================================================================
Test Hello World From API :: Test hello world endpoint                | PASS |
------------------------------------------------------------------------------
Test Echo From API :: Test echo endpoint                              | PASS |
------------------------------------------------------------------------------
Test Generic Endpoints From API :: Test multiple endpoints            | PASS |
------------------------------------------------------------------------------
Tests.testAPI :: Test API endpoints                                   | PASS |
3 tests, 3 passed, 0 failed
==============================================================================
Tests.testCAN :: Test canProgram who echo CAN messages with revert data bytes 
==============================================================================
Test CAN 1 :: Basic CAN test case 1                                   | PASS |
------------------------------------------------------------------------------
Test CAN 2 :: Basic CAN test case 2                                   | PASS |
------------------------------------------------------------------------------
Test CAN 3 :: Falling CAN with wrong message id                       | FAIL |
Keyword 'Mock Check Receive CAN Message' failed after retrying for 2 seconds. The last error was: No message received
------------------------------------------------------------------------------
Tests.testCAN :: Test canProgram who echo CAN messages with revert... | FAIL |
3 tests, 2 passed, 1 failed
==============================================================================
Tests                                                                 | FAIL |
6 tests, 5 passed, 1 failed
==============================================================================
Output:  /workspace/robot_framework/output.xml
Log:     /workspace/robot_framework/log.html
Report:  /workspace/robot_framework/report.html

Une fois les tests terminés, vous pouvez consulter les rapports générés:

  • log.html : Contient le log détaillé de l’exécution des tests, avec les résultats de chaque test.
  • report.html : Contient un résumé des tests exécutés, avec le nombre de tests réussis, échoués et ignorés.
  • output.xml : Contient les résultats des tests au format XML, utile pour l’intégration avec d’autres outils.

 

En ouvrant ces fichiers html, vous devriez voir les détails de l'exécution des tests, y compris les tests réussis et échoués, ainsi que les messages d'erreur:

 

 

Intégration continue

J’ai parlé de l’intégration avec des outils CI/CD comme GitLab CI, Jenkins etc… Robot Framework s’intègre bien avec ces outils, vous pouvez configurer votre pipeline pour exécuter les tests automatiquement et générer les rapports.

Voici un exemple de configuration pour GitLab CI, on définit dans les artifacts le rapport xUnit pour qu’il soit affiché dans l’interface GitLab:

stages:
  - test
test:
  stage: test
  script:
    - robot --xunit report_xunit.xml --debugfile debug.txt --randomize all tests/
  artifacts:
    when: always
    paths:
      - log.html
      - report.html
      - output.xml
      - report_xunit.xml
      - debug.txt
    expire_in: 2 week
    reports:
      junit: report_xunit.xml

Le résultat des tests sur GitLab :

Les bonnes pratiques de tests

Pour garantir la maintenabilité, la lisibilité et l’efficacité des tests, voici quelques recommandations

  • Organisez vos tests de manière logique, en regroupant les cas similaires dans des suites de tests, avec des noms explicites pour les fichiers et les cas de test
  • Rendez vos tests indépendants les uns des autres : ils doivent pouvoir s’exécuter dans n’importe quel ordre, si un test échoue, les autres ne doivent pas être impactés
  • Utilisez/Créez des keywords pour réduire la duplication de code et améliorer la lisibilité, donnez-leur des noms d’actions clairs, par exemple : Send CAN Message, Wait For CAN Message, Force Stop Server
  • Préférez Wait Until Keyword Succeeds à Sleep pour attendre qu’une condition soit remplie, cela évite les pauses inutiles et rend vos tests plus robustes
  • Évitez les valeurs en dur : utilisez des variables ou la section *** Variables *** pour rendre vos tests plus souples et faciles à maintenir
  • Documentez vos tests avec la balise [Documentation] pour expliquer le but et le fonctionnement de chaque cas de test et keyword
  • Utilisez les tags pour catégoriser vos tests, ce qui facilite leur exécution sélective
  • Utilisez des mocks pour simuler les dépendances externes, comme les devices, les API, les bases de données etc…

Conclusion

Nous avons vu dans cet article les bases de Robot Framework, ses concepts et son utilisation avec des mocks Python pour simuler des des communications CAN et des appels d’API, ainsi que les bonnes pratiques pour les tests et l’intégration avec des outils CI/CD.

J’ai essayé d’être le plus complet possible en vous donnant des exemples d’utilisation que j’ai pu rencontrer chez des clients et les keywords que j’ai le plus utilisés, mais c’est un framework très complet et je vous invite à explorer davantage ses fonctionnalités:

Source

Navigation of article

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.