jeudi 23 janvier 2014

Un fichier de tests pour l'include d'arithmétique temporelle


La programmation est très rigoureuse, et nous humains, nous le somme beaucoup moins. Il est difficile de penser à tout, et nous oublions de nombreuses choses. Surtout nous ne pouvons pas tout connaitre et c'est l'expérience qui nous fait apprendre de nouvelles choses. C'est pour cela qu'on ne peut pas espérer coder ne serait-ce qu'un petit programme sans aucun bug voire défaut de conception algorithmique. Dès qu'on fait du code, il faut s'attendre à ce que ça ne fonctionne pas, et qu'il faille tester pour trouver les problèmes de conception et bugs cachés. Depuis les années 90, le monde du développement informatique applique ce qu'on appelle le Test Driven Developpment: le développement conduit par les tests. Nous n'appliquerons pas à la lettre les recommandations qui disent par exemple de commencer à coder le programme de test avant même le code qu'il est sensé tester. Et nous avons tord, mais pour des raisons pédagogiques, je ne commence pas par faire les tests, ni ne fait des tests absolument pour tout. Pour les petits experts advisors ou indicateurs de démonstration le simple fait de les voir fonctionner de suite après nous confirme si la fonction principale est assurée sur le moment (on en sait rien dans le temps, ni des bugs cachés). Donc pour eux, pas de tests rigoureux. En général le test minimal est le test dit unitaire. Il consiste à juste tester que le code fonctionne dans un cas banal. Il n'y a pas de tests étendus, de torture avec un fonctionnement marathon ou avec le test du débile où des données farfelues sont fournies au code pour voir s'il réagit bien. Pas de tests avec tous les différents cas possibles, etc. Dans notre cas nous allons faire un peu plus que le test unitaire sans pour autant tester tous les cas. La stabilité dans le temps pourra être testé avec le testeur de stratégies de metaTrader, mais ça ne concerne que les indicateurs et experts advisors. Une librairie de fonction doit juste donner des résultats de traitement cohérents. Nous testerons donc plusieurs cas, et, plus nous en mettrons des différents, plus nous seront sûr de la qualité du code et de l'algorithme.

Tester la fonction obtenirNoDerniereSecConnue():


Nous allons tester cette fonction en l'appelant en lui passant ou non un paramètre, et même différents paramètres. Sans paramètre pour tester si la valeur par défaut est bien gérée, puis avec différents textes pour tester différents cas, et même des cas ou le texte n'est pas valide. Dans ce dernier cas, nous supposons déjà que la fonction va planter et nous verrons comment l'erreur est gérée par Metatrader. Puis nous modifierons la fonction pour qu'elle gère elle-même les erreurs qui peuvent survenir. Il faut le faire car si un bug est caché dans un indicateur ou expert advisor, et qu'il provoque le fait qu'un paramètre non valide est fourni à cette fonction, il faut pouvoir réagir en conséquence et ne pas faire perdre du capital par des erreurs en cascade:
  • Un cas sans paramètre et on doit obtenir la même chose que ce que fournit TimeCurrent().
  • Avec "EURUSD" et obtenir la même chose que ce que nous fournit  MarketInfo() .
  • Avec "GBPJPY" et obtenir la même chose que ce que nous fournit  MarketInfo() .
  • Avec une chaîne vide "" et voir la gestion de l'erreur par MetaTrader4 pour commencer puis obtenir le résultat par défaut lors d'une erreur que nous aurons décidé.
  • Avec "EUR" et obtenir le résultat par défaut pour une erreur.
Comme nous allons faire les tests dans un expert advisor, qui aura pour but de tester toutes les fonctions de ce fichier include, nous allons mettre tous ces cas de test dans une fonction nommée pour l'occasion TestObtenirNoDerniereSecConnue() (ouf ! 30 caractères, ça a faillit dépasser 31 caractères). Et à l'intérieur nous allons y tester les différents cas et comptabiliser les échecs aux tests. En cas d'échec on affiche un message avec les valeurs et en cas de succès aussi. Pour chaque cas on va:
  • obtenir le résultat du cas
  • tester si le résultat est égal à la valeur attendue
  • si oui, afficher que c'est ok et le résultat
  • si non afficher qu'il y a eu une erreur et le résultat, et augmenter le nombre d'erreurs de 1.
le code pour tester chaque cas sera donc:

resultat = obtenirNoDerniereSecConnue(//paramètre);
if(resultat == //resultat attendu) {
   Print(//nom de la fonction, //message ok, resultat);
} else {
   nbErreurs++;
   Print(//nom de la fonction, //message erreur, //paramètre fourni, resultat);
}

Et il faut répéter ce code avec les bonnes valeurs pour chaque cas de test, ce qui n'est pas très "clean code". J'ai réfléchi pour factoriser ce code, mais cela rallonge énormément la liste des déclarations de constantes et au final le code est très long. je ferai un post plus tard pour essayer de factoriser ces cas de tests, mais ce n'est pas facile d'obtenir quelque chose de convenable avec un langage comme MQL4. Pour l'instant on se contente de ce code linéaire et redondant qui malgré tout fonctionne.

Les résultats du test de la fonction obtenirNoDerniereSecConnue():


J'ai écrit le code et testé la fonction, et je constate que MetaTrader4 ne m'indique aucune erreur et que les quatre tests sont effectués, dont les deux derniers en erreur signalée. Ce qui signifie une chose: la fonction MarketInfo() réagit bien aux paramètres erronés. Elle les accepte et retourne quand même un résultat qui est zéro. J'avais l'intention de modifier le code pour qu'il réagisse à une erreur retournée par la fonction MarketInfo() et de renvoyer la valeur 0 car jamais utilisée en tant qu'horodatage ce qui aurait signifié au code appelant qu'il y a eu une erreur, mais MarketInfo() retourne déjà zéro ! C'est parfait ! On a donc déjà zéro dans la variable horodatage et c'est donc la valeur qui sera retournée. Nous n'avons pas besoin de modifier le code. Nous modifions juste les valeurs de résultat attendu qui doivent être zéro dans les cas où le paramètre fourni est mauvais.

Le tutoriel vidéo et le code complet:


Le code sera donc placé dans un expert advisor, on doit évidemment inclure le fichier ArithmétiqueTemporelle avec la directive préprocesseur #include suivie du nom du fichier entre deux chevrons ouvrant et fermant: #include <ArithmetiqueTemporelle.mqh> au début du fichier après le copyright. Nous créons alors une fonction testObtenirNoDerniereSecConnue() qui contient les tests de tous les cas et qui retourne le nombre d'erreur. Puis dans la fonction init(), on fait exécuter toutes les fonctions de tests (pour l'instant une seule) en récupérant le nombre d'erreurs à chaque fois et en les accumulant. A la fin de la fonction init() on affiche le bilan du test de l'include entier.

Voici le tutoriel vidéo:


Et le code complet:

//+------------------------------------------------------------------+
//|                                  testsArithmetiqueTemporelle.mq4 |
//|                  Copyright 2014, argent-facile-avec-robots-forex |
//|               http://argent-facile-avec-robots-forex.blogspot.fr |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, argent-facile-avec-robots-forex"
#property link   "http://argent-facile-avec-robots-forex.blogspot.fr"

#include <ArithmetiqueTemporelle.mqh>

//+------------------------------------------------------------------+
//| constantes de tests                                              |
//+------------------------------------------------------------------+

string msgTestOk = " a passé le test sans erreurs. Valeur retournée: ";
string msgErreurTest = " a produit une erreur de résultat avec le cas ";

string nomObtenirNoDerniereSecConnue = "obtenirNoDerniereSecConnue()";

//+------------------------------------------------------------------+
//| fonctions de tests                                               |
//+------------------------------------------------------------------+

int testObtenirNoDerniereSecConnue() {
   int nbErreurs = 0;
   datetime resultat;
   resultat = obtenirNoDerniereSecConnue();
   if(resultat == TimeCurrent()) {
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, resultat);
   } else {
      nbErreurs++;
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, " sans paramètre => ", resultat);
   }
   resultat = obtenirNoDerniereSecConnue("EURUSD");
   if(resultat == MarketInfo("EURUSD", MODE_TIME)) {
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, resultat);
   } else {
      nbErreurs++;
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, "\"EURUSD\" => ", resultat);
   }
   resultat = obtenirNoDerniereSecConnue("GBPJPY");
   if(resultat == MarketInfo("GBPJPY", MODE_TIME)) {
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, resultat);
   } else {
      nbErreurs++;
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, "\"GBPJPY\" => ", resultat);
   }
   resultat = obtenirNoDerniereSecConnue("EUR");
   if(resultat == 0) {
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, resultat);
   } else {
      nbErreurs++;
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, "\"EUR\" => ", resultat);
   }
   resultat = obtenirNoDerniereSecConnue("");
   if(resultat == 0) {
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, resultat);
   } else {
      nbErreurs++;
      Print(nomObtenirNoDerniereSecConnue, msgTestOk, "\"\" => ", resultat);
   }
   return(nbErreurs);
}

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+

int init()
  {
   int nbErreurs = 0;
   nbErreurs += testObtenirNoDerniereSecConnue();
   Print("TESTS DE ArithmetiqueTemporelle.mqh TERMINES AVEC ", nbErreurs, " ERREUR(S).");
   return(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+

int deinit()
  {
   return(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+

int start()
  {
   return(0);
  }

//+------------------------------------------------------------------+

Je ne peux définitivement pas laisser un tel code, redondant à ce point ! On est loin du «clean code», c'est même du «bad code». Surtout qu'on va ajouter d'autres fonctions dans l'include d'arithmétique temporelle, et donc multiplier ce code de test. Il est important, très important de maintenir un code de test clean, tout comme le code utile. Je m'y attèle dès le prochain post, nous factoriserons ce code. Pour l'instant l'objectif est rempli: notre première fonction de l'include est testée et on sait qu'on peut avoir confiance en elle. Nous n'avons pas pourtant de garantie à 100%, on va dire que nous sommes à 99%. Mais sans ces tests, c'était du 1%. (la première version de la fonction avait échouée non pas aux tests, mais carrément à la compilation !  =D)

Aucun commentaire:

Enregistrer un commentaire