Ecrire ses keywords Robot Framework avec Python
Tags: python, testing, tutorial
Published: 2020-11-19 10:17:19

Voici une histoire inspirée de ma vie professionnelle :

Moi :

> "J'ai bien avancé dans le développement de la communication Bluetooth entre notre frigo connecté et l'application PC avec une super bibliothèque en Python ! Maintenant j'aimerais bien écrire des tests et les automatiser."

Des gens de mon équipe :

> "Utilise Robot Framework ! Tu verras : c'est cool !"

Moi :

> "OK, je tente ça !"

Il y a plein de tutoriels Robot Framework sur le net, mais en général on te montre comment te connecter une page web pour t'authentifier ou comment interroger un _endpoint_ REST pour faire 2+2=4 (j'exagère à peine 😀). Autant dire que j'étais très loin de me connecter en Bluetooth à mon frigo...

Dans cet article, je vais vous expliquer comment interfacer Robot Framework avec une bibliothèque en Python pour automatiser les tests d'un frigo connecté (qui est fictif, vous l'avez compris).

## Robot Framework en bref

Je vais laisser Robot Framework se présenter, en citant [sa page officielle](https://robotframework.org/) :

> Robot Framework is a generic open source automation framework. It can be used for test automation and robotic process automation (RPA).
>
> [...]
>
> Robot Framework itself is open source software released under [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html), and most of the libraries and tools in the ecosystem are also open source. The framework was initially developed at Nokia Networks and was open sourced in 2008.

Robot Framework est principalement écrit en Python et le code est disponible sur [GitHub](https://github.com/robotframework/robotframework).

On peut l'installer facilement avec `pip` puisque les packages sont disponibles sur [PyPi](https://pypi.org/project/robotframework/) :

```bash
$ pip install robotframework
```

![rf logo](https://upload.wikimedia.org/wikipedia/commons/e/e4/Robot-framework-logo.png)

## Les keywords : le nerf de la guerre

L'écriture de tests en Robot Framework repose sur l'utilisation de _keywords_. Les _keywords_ (qui ressemblent des fois beaucoup à des _keyphrases_) permettent de réaliser une action ou une vérification.

Voici un exemple d'un *test case* trouvé sur le [site officiel](https://robotframework.org/#examples) :

```robot
*** Test Cases ***
Valid Login
   Open Browser To Login Page
   Input Username    demo
   Input Password    mode
   Submit Credentials
   Welcome Page Should Be Open
   [Teardown]    Close Browser
```

Chaque ligne est composée d'un _keyword_ et d'éventuels paramètres. Il est possible d'écrire ses propres _keywords_ à partir de _keywords_ existants. On pourrait par exemple écrire le _keyword_ suivant :

```robot
*** Keywords ***
Input Credentials
   [Arguments]    ${username}    ${password}
   Input Username    ${username}
   Input Password    ${password}
```

On peut ainsi réécrire de manière simplifiée le test case précédent avec ce nouveau _keyword_ :

```robot
*** Test Cases ***
Valid Login
   Open Browser To Login Page
   Input Credentials    demo    mode
   Submit Credentials
   Welcome Page Should Be Open
   [Teardown]    Close Browser
```

Ici, je ne fais qu'écrire des _keywords_ à partir de _keywords_ existants. Mais comment faire si on veut créer des _keywords_ vraiment nouveaux, genre des _keywords_ pour se connecter en Bluetooth à un frigo et lui envoyer une nouvelle consigne de température ?

Sur [la page officielle](https://robotframework.org/) de Robot Framework, on peut lire :

> Its capabilities can be extended by libraries implemented with Python or Java.

Très bien. On fait comment ?

## Créer sa bibliothèque de keywords avec Python

J'avoue que mes premiers pas pour interfacer Robot Framework avec ma bibliothèque Python existante ont été laborieux. [La documentation officielle](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#creating-test-libraries) est très détaillée, mais elle est difficile à appréhender pour débuter. Elle tient clairement plus du _Reference Manual_ que du _Getting Started_. Je me suis retrouvé à regarder des [vidéos YouTube de _live coding_ d'une boite de consulting indienne](https://www.youtube.com/watch?v=pvSWEXzs3Bs&list=PLIMhDiITmNrLD-gOfSN10LuIsq_fXbTjd&index=11&t=0s).

![_True story_](https://media.tenor.com/images/24805223bc5f729f16fe658893de5c54/tenor.gif)

En fait, il existe plusieurs façons d'appeler du code Python depuis Robot Framework. La solution simple est suffisante dans beaucoup de cas. C'est celle que j'ai utilisée et c'est celle que je vais vous présenter ici. Elle est nommée "_Static API_" dans la documentation :

> The simplest approach is having a module (in Python) or a class (in Python or Java) with methods which map directly to keyword names. Keywords also take the same arguments as the methods implementing them. Keywords report failures with exceptions, log by writing to standard output and can return values using the return statement.

Petites remarques :

- Les fonctions Python dont le nom commencent par un `_` sont cachées dans Robot Framework.
- Une fonction Python s'appelant `input_credentials()` peut être utilisée en Robot Framework comme `Input Credentials`, `input credentials`, `Input credentials` ou encore `input_credentials` (bref, vous avez le choix).
- si comme moi vous avez déjà une bibliothèque Python, il est certainement intéressant de ne pas l'importer directement dans Robot Framework mais de faire un wrapper intermédiaire pour exposer de jolis _keywords_ bien pensés, comme suggéré dans [cette discussion sur stackoverflow](https://stackoverflow.com/questions/27039016/how-to-create-a-custom-python-code-library-for-the-robot-framework).

## Exemple simple

Voici un exemple de bibliothèque Python prête à être importée dans Robot Framework, qu'on appellera `fridge.py` :

```python
import robot.api.logger
import robot.utils.asserts

# Variable that is supposed to be in the fridge firmware
_temperature_setpoint = 5


def _ble_send_setpoint(value):
    """Pretend to send data to the fridge with the new temperature setpoint.

    :param value: the setpoint value
    """
    global _temperature_setpoint
    _temperature_setpoint = value
    print('Sending setpoint to the fridge: ', _temperature_setpoint)


def _ble_read_setpoint():
    """Pretend to read data to the fridge to get the temperature setpoint.

    :return: the setpoint value
    """
    global _temperature_setpoint
    print('Reading setpoint to the fridge: ', _temperature_setpoint)
    return _temperature_setpoint


def change_temperature_setpoint(value):
    """Function exposed as a keyword to change the temperature setpoint.

    :param value: the setpoint value
    """
    value = float(value)
    robot.api.logger.info('Change temperature setpoint to %f' % value)
    _ble_send_setpoint(value)


def check_temperature_setpoint(expected):
    """Function exposed as a keyword to check the temperature setpoint.

    :param expected: the expected setpoint value
    """
    expected = float(expected)
    actual = _ble_read_setpoint()
    robot.utils.asserts.assert_equal(expected, actual)
```

Les deux premières fonctions simulent une communication avec le frigo connecté pour lire et écrire la consigne de température. Une variable locale stocke cette valeur, simulant la mémoire du processeur du frigo. Comme leurs noms commencent par un _underscore_, elles ne seront pas importées comme _keywords_. Elles ne sont pas nécessaires ici, elles servent simplement à vous montrer le principe.

Les deux dernières fonctions en revanche seront bien importées comme des _keywords_.

Remarquez la conversion des arguments avec la fonction `float()`. En effet, les arguments sont transmis de Robot Framework vers Python [comme étant des _strings_ par défaut](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#argument-types) et ce n'est pas le format qui nous arrange ici. On transforme donc ces _strings_ en nombres.

Maintenant, écrivons donc un petit _test case_ pour se servir de nos super _keywords_ !

```robot
*** Settings ***
Library  fridge.py

*** Test Cases ***
It is possible to change the temperature setpoint of the fridge
    change_temperature_setpoint  4
    check_temperature_setpoint   4

    change temperature setpoint  8
    Check temperature setpoint   8

    Change_Temperature SETPOINT  3.5
    CHECK temperature_SETpoint   3.5
```

Je vous l'avais dit : les _underscores_ et la casse sont gérés en toute souplesse 😅

Enregistrons ces tests dans un fichier `tests.robot` (ce fichier sera considéré comme une _test suite_ nommée _Tests_) et exécutons-les depuis notre terminal :

```bash
$ robot tests.robot
==============================================================================
Tests
==============================================================================
It is possible to change the temperature setpoint of the fridge       | PASS |
------------------------------------------------------------------------------
Tests                                                                 | PASS |
1 critical test, 1 passed, 0 failed
1 test total, 1 passed, 0 failed
==============================================================================
Output:  C:\...\output.xml
Log:     C:\...\log.html
Report:  C:\...\report.html
```

Si vous n'avez jamais utilisé Robot Framework, il génère un superbe rapport HTML avec tous les détails. Le résumé ressemble à ça :

![report summary](https://dev-to-uploads.s3.amazonaws.com/i/39cp2cgayi4d77edkm6j.png)

Pour chaque test, on a le détail complet des opérations faites et on retrouve les logs faits depuis le code Python, la documentation des fonctions grâce aux docstring, etc :


![report details](https://dev-to-uploads.s3.amazonaws.com/i/ekqwblmnka5xjvof9icn.png)

## Maintenant, le monde est à vous

A partir de là, le reste n'est que du Robot Framework, du Python, un peu d'imagination et pas mal de lecture de documentation. Vous avez toutes les bases pour aller plus loin dans l'écriture de vos tests automatisés.

A titre d'exemple des possibilités qui s'ouvrent à vous, je vais vous montrer comment "automatiquement" se connecter au frigo au début de chaque _test case_ et se déconnecter à la fin de celui-ci. Car oui, avant de communiquer avec un système, il faut s'y connecter et on n'a pas envie de le faire manuellement dans chaque _test case_. Déjà parce que c'est pénible, mais surtout parce qu'il y a un risque d'oubli et donc d'erreur.

Pour faire ça, je rajoute 2 fonctions à ma bibliothèque `fridge.py` :

```python
def connect():
    """Pretend to connect to the fridge."""
    robot.api.logger.info('Connect to the frige')


def disconnect():
    """Pretend to disconnect from the fridge."""
    robot.api.logger.info('Disconnect from the frige')
```

Je configure ma _test suite_ pour que le _setup_ d'un _test case_ établisse la connexion et que le _teardown_ procède à la déconnexion :

```robot
*** Settings ***
Library  fridge.py

Test Setup      Connect
Test Teardown   Disconnect

*** Test Cases ***
It is possible to change the temperature setpoint of the fridge
    Change temperature setpoint  6
    Check temperature setpoint   6
```

Je constate dans le rapport que mes fonctions sont bien appelées :

![report setup teardown](https://dev-to-uploads.s3.amazonaws.com/i/02fv8draofy1rpki98ck.png)

## Conclusion

Vous savez maintenant comment écrire des fonctions en Python et les importer dans Robot Framework pour les utiliser en tant que _keywords_.

La solution la plus simple est d'écrire un fichier `mon_module.py` et de l'importer dans Robot Framework dans la section `*** Settings ***` de votre fichier de tests avec la commande `Library  mon_module.py`. Toutes les fonctions dont le nom ne commencent pas par un _underscore_ seront exposées en tant que _keywords_.

Allez, je vous laisse, j'ai un frigo à tester ! 😎