Monsieur Jourdain: Adieu Sganarelle, quoi de prévu pour aujourd'hui?

Sganarelle: Les tests unitaires.

Monsieur Jourdain: Ah... Mais vous êtes au courant que même si l'on sait que c'est bien, on n'aime pas faire ça...

Sganarelle: Ouais, mais là j'ai un argument choc: le développement sur microcontrôleurs!

Monsieur Jourdain: ...

Ça annonce du lourd, n'est-ce pas?

J'ai eu très récemment besoin d'écrire une classe de pilotage de moteur pas à pas pour un microcontrôleur type Arduino (Teensy pour être exact, mais là n'est pas la question).

La classe en question doit gérer l'accélération et décélération du moteur en début et fin de mouvement.

Vous allez me dire, il existe déjà des bibliothèques pour ça. Hé bien oui, mais vous commencez à me connaitre, et j'étais très curieux de développer mon propre contrôleur de moteur. C'était d'ailleurs très instructif de se replonger dans les maths de terminale S pour calculer tout ce bordel; notamment réapprendre la notion d'intégrale pour calculer la distance parcourue pendant la durée du mouvement, avec une accélération et décélération constantes en début et fin de mouvement.

J'en ai aussi profité pour découvrir des outils mathématiques en ligne extrêmement pratiques, comme Desmos ça, c'est la courbe de vitesse par rapport au temps

En résumé, j'ai dû écrire une série de fonctions de calcul purement mathématiques pour calculer la durée d'accélération, le nombre de pas selon l'accélération et la vitesse max, ce genre de trucs.

J'aurais pu y aller empiriquement, et débugger avec des traces dans la console Arduino, à base de Serial.println(), mais franchement... franchement... C'est super relou de débugger sur ce genre de plateforme...

Alors dans un premier temps, j'ai écrit ces fonctions en Python (oui, je m'y suis mis et je kiffe pas mal), et comme Python dispose en standard d'un environnement de tests unitaires, c'était cool.

Puis, une fois mes fonctions validées en Python, je les ai transposées en C++ dans une classe statique. Mais là, c'est méga chiant, parce que les langages sont quand même très différents :/

J'ai cherché un environnement de tests unitaires en C++ (j'en avais de mauvais souvenirs en termes de dépendance, et c'est essentiellement pour ça que je suis passé par Python). Et j'ai trouvé la perle: Catch

C'est HYPER simple à mettre en place. Aucun héritage, ni nommage conventionnel à faire. Juste UN fichier d'en-tête à inclure, et une syntaxe à la cool pour écrire les tests :)

Imaginons que j'aie à tester une classe qui permet d'effectuer des opérations très complexes, telles qu'ajouter deux nombres, calculer une puissance ou pire, diviser deux nombres (float).

#define CATCH_CONFIG_MAIN
#include "catch.hpp"
#include "mymath.h"

TEST_CASE("Tests addition", "[addition]"){

  REQUIRE(MyMath::add(1, 2) == 3);
  REQUIRE(MyMath::add(-1, 2) == 1);
  REQUIRE(MyMath::add(-1, -2) == -3);
}

TEST_CASE("Tests puissance", "[power]"){

  REQUIRE(MyMath::power(2, 0) == 1);
  REQUIRE(MyMath::power(2, 1) == 2);
  REQUIRE(MyMath::power(2, 2) == 4);
  REQUIRE(MyMath::power(2, 3) == 8);
  REQUIRE(MyMath::power(2, 10) == 1024);
  REQUIRE(MyMath::power(2, 12) == 4096);
}

TEST_CASE("Tests division", "[division]"){

  CHECK(MyMath::div(2.0, 1.0) == 2.0);
  CHECK(MyMath::div(1.0, 3.0) == Approx(0.333).epsilon(0.01));
  CHECK(MyMath::div(9.0, 2.0) == 4.5);
}

Pour info, REQUIRE arrête l'exécution des tests en cas d'erreur, alors que CHECK indique l'erreur et continue avec les autres tests. Pour plus de détails (notamment sur l'approximation des nombres à virgule flottante), voir la documentation officielle.

Et qu'est-ce qu'il dit?

===============================================================================
All tests passed (12 assertions in 3 test cases)

Article précédent Article suivant