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: