IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

C Discussion :

Apprendre à tester ses programmes - Besoin de conseils


Sujet :

C

  1. #1
    Invité
    Invité(e)
    Par défaut Apprendre à tester ses programmes - Besoin de conseils
    Bonjour,

    Je suis débutant dans la programmation et aurais besoin d'un coup de main. Dans le cadre de ma formation, on nous demande de coder une soixantaine de fonctions en C, souvent extraites de la librairie standard du langage.
    J'ai terminé cette partie du boulot mais fais face à un problème. Je voudrais écrire moi-même un programme de tests pour toutes les fonctions. Je peux trouver en ligne des programmes qui font le job, mais voudrais plutôt être capable d'écrire le mien. Je me dis que dans mon futur boulot, il n'y aura personne pour tester à ma place, alors autant prendre le temps d'acquérir de bons réflexes dans le domaines dès aujourd'hui, sur des travaux peu conséquents.

    Du coup, auriez-vous des conseils pour orienter ce travail? J'imagine que je pourrais écrire des tests simples qui comparent les résultats que je devrais avoir avec ceux que j'ai, mais il y a peut-être des procédures un peu plus solides et éprouvées qui me donneraient les bons réflexes à adopter pour tester des projets plus gros. Quand je regarde le code source des programmes testeurs déjà créés, la structure est vraiment intéressante et assez riche, c'est un résultat auquel j'aimerais parvenir.

    J'espère que cette demande n'est pas trop vague.
    Merci d'avance pour les éventuelles réponses et bonne journée

  2. #2
    Membre Expert
    Inscrit en
    Mars 2005
    Messages
    1 431
    Détails du profil
    Informations forums :
    Inscription : Mars 2005
    Messages : 1 431
    Par défaut
    Ce que tu évoques - la validation d'une brique constitutive du programme sur un jeu d'entrée limité - s'appelle test unitaire. Son emploi au cours du cycle de développement est une composante de l'intégration continue.

    Citation Envoyé par MadiPoupou Voir le message
    J'imagine que je pourrais écrire des tests simples qui comparent les résultats que je devrais avoir avec ceux que j'ai, mais il y a peut-être des procédures un peu plus solides et éprouvées qui me donneraient les bons réflexes à adopter pour tester des projets plus gros.
    Ben voilà : c'est aussi simple que cela ! Le gros du travail consiste en réalité à intégrer la compilation et l'exécution automatique de ces tests dans la procédure de génération de l'exécutable principal.

    Code square.h : Sélectionner tout - Visualiser dans une fenêtre à part
    int square(int x);
    Code test/square.c : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <assert.h>
     
    #include "../square.h"
     
    int main() {
        assert(square(3) == 9);
        assert(square(7) == 49);
        assert(square(10) == 100);
        return 0;
    }


    Il existe d'innombrables outils plus ou moins sophistiqués pour encadrer l'écriture et la gestion de ces tests, tu en trouveras quelques uns là-bas : https://en.wikipedia.org/wiki/List_o...g_frameworks#C . Check, par exemple, est un candidat sérieux pour toute taille de projet.

  3. #3
    Invité
    Invité(e)
    Par défaut
    Merci pour ta réponse. Je crois qu'en fait j'avais cherché la complication là où elle n'était pas.
    Je vais essayer de travailler sur les test unitaires.

    Merci beaucoup et bon weekend!

  4. #4
    Expert confirmé
    Homme Profil pro
    Analyste/ Programmeur
    Inscrit en
    Juillet 2013
    Messages
    4 836
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Analyste/ Programmeur

    Informations forums :
    Inscription : Juillet 2013
    Messages : 4 836
    Par défaut
    La réponse de Matt_Houston est correcte ... mais cela demande beaucoup de veille technologique pour se renseigner/ tester/ ...

    Un truc rapide est simplement de créer un 2ième projet (en ligne de commandes) pour juste tester ton code : cela aura l'avantage d'aller plus vite et sûrement de commencer à te faire réfléchir :
    • Est-ce qu'il faut ajouter à mes classes des opérateurs pour les tests ?
    • Comment créer les arguments à passer en paramètres ? Avec quelles valeurs ?
    • Comment tester une classe virtuelle ?
    • Comment tester une boite noire ?
    • ...

    Par contre, il ne sera pas automatisé : ce sera à toi de le lancer et de regarder les success/ fail

    Un truc important : les tests permettent à la fois de tester le bon comportement (surtout avec les valeurs particulières) mais également toutes les erreurs possibles : ton nombre de tests peut vite grandir/ exploser

    Un exemple pour tester la fonction extract_input : on va coder une procédure (<- fonction sans argument) et sans retour test_extract_input, qui va chaîner les tests (*) avec un compteur, et que l'on va appeler dans le main.
    Attention, cela peut faire du code très long, moche et avec une maintenance douteuse. C'est du code fait au copié/ collé donc attention aussi.
    À l'inverse tu auras un texte personnalisé pour chaque test (<- qui prend en compte les 80 colonnes de la ligne de commandes) : donc facilement compréhensible pour repérer le problème (ou facilement trouvable dans cette masse de code pour se remémorer le test)
    Un dernier point, les include sont sauvages : comme ce projet test est dans ton projet, mais dans un/des dossier(s) spécifique(s), il ne faut pas avoir peur des chemins un peu tortueux

    * : Cela semble évident , mais 1 test c'est juste un appel à la fonction suivi des tests nécessaires pour tester le(s) résultat(s). Ensuite pour d'autres tests (tests de performances par exemple), cela peut être différent.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    #include "../main/project_XXX/cmd_line_parser.h"
     
    #include "../main/project_XXX/common.h"
     
    #include <string>
     
     
    void test_extract_input(void) {
    	unsigned char raw_data[16];
    	unsigned short data_len, count = 1;
    	unsigned char alias_len;
    	bool is_an_alias;
    	INPUT_ERROR error;
     
    	alias_len = 20;
     
    	if ( extract_input("alia()", 10, alias_len, raw_data, data_len, is_an_alias, error) ) {
    		std::printf("test %03d func - invalid func:               Error\n", count);
    	} else {
    		if ((data_len == 0) && (error == I_ERR_INVALID_FUNC)) {
    			std::printf("test %03d func - invalid func:               Success\n", count);
    		} else {
    			std::printf("test %03d func - invalid func:               Error - Invalid output\n", count);
    		}
     
    	}
     
    	++count;
     
    	if ( extract_input("ascii_st()", 10, alias_len, raw_data, data_len, is_an_alias, error) ) {
    		std::printf("test %03d func - invalid func:               Error\n", count);
    	} else {
    		if ((data_len == 0) && (error == I_ERR_INVALID_FUNC)) {
    			std::printf("test %03d func - invalid func:               Success\n", count);
    		} else {
    			std::printf("test %03d func - invalid func:               Error - Invalid output\n", count);
    		}
     
    	}
     
    	++count;
     
    	if ( extract_input("dec_lis()", 10, alias_len, raw_data, data_len, is_an_alias, error) ) {
    		std::printf("test %03d func - invalid func:               Error\n", count);
    	} else {
    		if ((data_len == 0) && (error == I_ERR_INVALID_FUNC)) {
    			std::printf("test %03d func - invalid func:               Success\n", count);
    		} else {
    			std::printf("test %03d func - invalid func:               Error - Invalid output\n", count);
    		}
     
    	}
     
    	++count;
     
    // ...
     
    /****************************************************************************/
    /**************************   alias && ascii_str   **************************/
    /****************************************************************************/
     
    	if ( extract_input("ascii_str(test)", 10, alias_len, raw_data, data_len, is_an_alias, error) ) {
    		if (!is_an_alias && (data_len == 4) && (std::strncmp((const char*) raw_data, "test", 4) == 0) &&
    			(error == I_ERR_NONE)) {
     
    			std::printf("test %03d func ascii_str - valid:            Success\n", count);
    		} else {
    			std::printf("test %03d func ascii_str - valid:            Error - Invalid output\n", count);
    		}
    	} else {
    		std::printf("test %03d func ascii_str - valid:            Error\n", count);
    	}
     
    	++count;
     
    // ...
     
    	{
    		char str[] = "ascii_str( )";
    		unsigned char c;
    		bool is_ok = true;
     
    		c = 0x01;
     
    		while(is_ok && (c <= 0x7F)) {
    			str[10] = c;
     
    			if ( extract_input(str, 2, alias_len, raw_data, data_len, is_an_alias, error) ) {
    				if (!is_an_alias && (data_len == 1) && (raw_data[0] == c) &&
    					(error == I_ERR_NONE)) {
    				} else {
    					is_ok = false;
    				}
    			} else { 
    				is_ok = false;
    			}
     
    			++c;
     
    			if (c == ' ') { ++c; }
    		}
     
    		if (is_ok) {
    			std::printf("test %03d func ascii_str - valid chars:      Success\n", count);
    		} else {
    			std::printf("test %03d func ascii_str - valid chars:      Error\n", count);
    		}
    	}
     
    // ...
    }
    Un autre exemple avec des macros spécifiques qui lorsque c'est bien fait, allége le code et la visibilité (par contre le code de la macro est souvent beurk )

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    #include "../main/project_XXX/cmd_line_parser.h"
     
    #include <string>
     
     
    #define TEST_INVALID(PARAM, ERROR_ID) \
    	(PARAM.selected_switch == SWITCH_NONE) && (PARAM.list_values.size() == 0) && \
    	(PARAM.param_error == ERROR_ID) && (!PARAM.has_no_error) && \
    	(!PARAM.has_AID == 0) && (!PARAM.has_key == 0) && (PARAM.index_filename == 0) && \
    	(PARAM.has_port == false) &&  (PARAM.port == 0)
     
     
    #define TEST_PARAM(PARAM, SWITCH_ID, AID, APK1) \
    	(PARAM.selected_switch == SWITCH_ID) && (PARAM.list_values.size() == 2) && \
    	(PARAM.param_error == P_ERR_NONE) && PARAM.has_no_error && \
    	(PARAM.index_AID > 0) && (PARAM.index_AID <= 2) && (PARAM.list_values[(size_t) (PARAM.index_AID - 1)] == AID) && \
    	(PARAM.index_APK1 > 0) && (PARAM.index_APK1 <= 2) && \
    	(PARAM.list_values[(size_t) (PARAM.index_APK1 - 1)] == APK1) && \
    	(PARAM.index_filename == 0) && \
    	(PARAM.has_port == false) && (PARAM.port == 0)
     
     
    #define TEST_PARAM_FILENAME(PARAM, SWITCH_ID, FILENAME) \
    	(PARAM.selected_switch == SWITCH_ID) && (PARAM.list_values.size() == 1) && \
    	(PARAM.index_AID == 0) && (PARAM.index_APK1 == 0) && (PARAM.index_filename == 1) && \
    	(PARAM.list_values[(size_t) (PARAM.index_filename - 1)] == FILENAME) && \
    	(PARAM.has_port == false) && (PARAM.port == 0)
     
     
    #define TEST_PARAM_WITH_PORT(PARAM, PORT, SWITCH_ID, AID, APK1) \
    	(PARAM.selected_switch == SWITCH_ID) && (PARAM.list_values.size() == 2) && \
    	(PARAM.param_error == P_ERR_NONE) && PARAM.has_no_error && \
    	(PARAM.index_AID > 0) && (PARAM.index_AID <= 2) && (PARAM.list_values[(size_t) (PARAM.index_AID - 1)] == AID) && \
    	(PARAM.index_APK1 > 0) && (PARAM.index_APK1 <= 2) && \
    	(PARAM.list_values[(size_t) (PARAM.index_APK1 - 1)] == APK1) && \
    	(PARAM.index_filename == 0) && \
    	PARAM.has_port &&  (PARAM.port == PORT)
     
     
    #define TEST_params1_WITH_PORT(PARAM, PORT) \
    	(PARAM.selected_switch == SWITCH_GET_CARD_TYPE) && (PARAM.list_values.size() == 0) && \
    	(PARAM.param_error == P_ERR_NONE) && PARAM.has_no_error && \
    	(PARAM.index_AID == 0) && (PARAM.index_APK1 == 0) && (PARAM.index_filename == 0) && \
    	PARAM.has_port &&  (PARAM.port == PORT)
     
     
    #define TEST_params2_WITH_PORT(PARAM, PORT, FILENAME) \
    	(PARAM.selected_switch == SWITCH_DEBUG) && (PARAM.list_values.size() == 1) && \
    	(PARAM.param_error == P_ERR_NONE) && PARAM.has_no_error && \
    	(PARAM.index_AID == 0) && (PARAM.index_APK1 == 0) && (PARAM.index_filename == 1) && \
    	(PARAM.list_values[(size_t) (PARAM.index_filename - 1)] == FILENAME) && \
    	PARAM.has_port &&  (PARAM.port == PORT)
     
     
    #define TEST_VALID(PARAM, SWITCH_ID) \
    	(PARAM.selected_switch == SWITCH_ID) && (PARAM.list_values.size() == 0) && \
    	(PARAM.param_error == P_ERR_NONE) && PARAM.has_no_error && \
    	(PARAM.index_AID == 0) && (PARAM.index_APK1 == 0) && (PARAM.index_filename == 0) && \
    	(PARAM.has_port == false) &&  (PARAM.port == 0)
     
     
    void test_parser(void)
    {/*
    	Command_Feeder feeder(0, NULL);
     
    	Input_Params params1;
    	Input_Params params2;
    	Input_Params params3;
    	Input_Params params4;
    	Input_Params params5;
    	Input_Params params6;
    	Input_Params params7;
     
    	unsigned char c = 1;
     
    	{
    		char* argv1[4] = {"XXX", "-card_agent_get_id",  "0E7500", "12345678901234567890123456789012"};
    		char* argv2[4] = {"XXX", "-card_agent_get_i",   "0E7500", "12345678901234567890123456789012"};
    		char* argv3[4] = {"XXX", "-card_agent_get_id1", "0E7500", "12345678901234567890123456789012"};
     
    		parse(4, argv1, params1, false); // <- param.clear();
    		parse(4, argv2, params2, false); // <- param.clear();
    		parse(4, argv3, params3, false); // <- param.clear();
     
    		std::printf("Test %2u.1 - card_agent_get_id: %s\n", c, ((TEST_PARAM(params1, SWITCH_CARD_AGENT_GET_ID, argv1[2], argv1[3]))? "Success": "Error"));
    		std::printf("Test %2u.2 - card_agent_get_id: %s\n", c, (( TEST_INVALID(params2, P_ERR_INVALID_COMMAND) )? "Success": "Error"));
    		std::printf("Test %2u.3 - card_agent_get_id: %s\n", c, (( TEST_INVALID(params3, P_ERR_INVALID_COMMAND) )? "Success": "Error"));
    	}
     
    	++c;
     
    	{
    		char* argv1[2] = {"XXX", "-card_prog_read"};
    		char* argv2[2] = {"XXX", "-card_prog_rea"};
    		char* argv3[2] = {"XXX", "-card_prog_read1"};
     
    		parse(2, argv1, params1, false); // <- param.clear();
    		parse(2, argv2, params2, false); // <- param.clear();
    		parse(2, argv3, params3, false); // <- param.clear();
     
    		std::printf("Test %2u.1 - card_prog_read:    %s\n", c, (( TEST_VALID(params1, SWITCH_CARD_PROG_READ) )? "Success": "Error"));
    		std::printf("Test %2u.2 - card_prog_read:    %s\n", c, (( TEST_INVALID(params2, P_ERR_INVALID_COMMAND) )? "Success": "Error"));
    		std::printf("Test %2u.3 - card_prog_read:    %s\n", c, (( TEST_INVALID(params3, P_ERR_INVALID_COMMAND) )? "Success": "Error"));
    	}
     
    	++c;
     
    // ...
    }

    Toi tu peux faire un fichier test pour 1 fonction (soit une 60taine )

    Et dans ton main tu peux utiliser les arguments de la ligne de commandes pour lancer tous les tests, une série de tests, ou ....
    Par contre , je ne peux pas trop te montrer de code parce que j'ai recodé en C++ très simplement la bibliothèque getopt.


    Édit 1: Pour certains tests, peut-être que le C++ est plus approprié pour, par exemple, manipuler des chaînes de caractères. Donc même si ton projet principal est en pur C, rien ne t'empêche de faire un projet test en C++ : cela fonctionne tout également.
    Je vois que GoogleTests chronomètre les tests : pour des tests de performance cela est obligatoire effectivement .
    En C/ C++ cela va dépendre de ton système d'exploitation. Donc j'utilise la fonction getRealTime de David Robert Nadeau (<- lien) : à voir si actuellement il n'y a pas mieux.

    Édit 2: Le problème de la méthode à la main c'est que tu peux faire des tests qui peuvent être difficiles à mettre en place ou nécessite beaucoup de code avec une bibliothèque/ cadriciel : donc attention au fait de dire "faire les tests à la main dans un premier temps" si tu utilises une méthode plus robuste.
    Un exemple : Comment tester que ton algo génère un identifiant unique ?
    Simple [mais très lent]: tu crées une base de données avec SQlite, avec 1 seule table et avec une colonne UNIQUE, et dans une boucle tu appelles ton algo et tu l'insères dans ta base de données en gérant les erreurs d'insertion.

  5. #5
    Invité
    Invité(e)
    Par défaut
    Ouaw
    Merci beaucoup pour toutes tes infos. Au final, j'avais plutôt penché pour ton option en créant des tests "à la main", pour une partie de mes fonctions. J'ai la chance d'avoir une programme externe de test assez robuste, alors je me contente de tester les fonctions qui ont un fonctionnement très "différent". Par exemple, je crée une mécanique de test d'une fonction qui renvoie une string, une pour une fonction qui renvoie un int, etc. J'ai encore du boulot, mais je suis arrivé à un résultat correct pour le moment.
    Je me contente d'utiliser du C parce que je ne connais rien en C++. Je suis vraiment en train d'approfondir les basiques du C, je vais patienter avant de me pencher sur autre chose.
    En revanche, je garde ton message au chaud parce que certaines notions qui m'échappent un peu pour le moment me seront probablement utiles par la suite.

    Et je crois comprendre le problème de cette méthodologie de test. Mais elle semble tout de même assez appropriée au travail que je dois réaliser (en gros, un bon vieux tas de fonctions qui n'ont pratiquement aucun lien les unes avec les autres, ou très peu).

    Je profite du post pour poser une question. Je dois tester des fonctions qui écrivent sur la sortie standard (en utilisant l'appel système write()). La méthode que j'ai choisie est de rediriger la sortie standard vers un file descriptor différent, un fichier perso, puis de comparer le contenu de ce fichier avec ce que j'attendais. Est-ce une bonne méthode?
    Et lorsque l'on souhaite tester une fonction qui efface de la mémoire. Comment peut-on vérifier qu'elle fait bien son travail? Parce qu'en plus, cette dernière est supposée mettre à NULL le pointeur qui servait à identifier la zone mémoire... Je peux vérifier si ce dernier est bien NULL, mais la zone mémoire...

    Merci beaucoup pour vos réponses! Bon début de semaine

  6. #6
    Expert confirmé
    Homme Profil pro
    Analyste/ Programmeur
    Inscrit en
    Juillet 2013
    Messages
    4 836
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Analyste/ Programmeur

    Informations forums :
    Inscription : Juillet 2013
    Messages : 4 836
    Par défaut
    Citation Envoyé par MadiPoupou Voir le message
    Je profite du post pour poser une question. Je dois tester des fonctions qui écrivent sur la sortie standard (en utilisant l'appel système write()). La méthode que j'ai choisie est de rediriger la sortie standard vers un file descriptor différent, un fichier perso, puis de comparer le contenu de ce fichier avec ce que j'attendais. Est-ce une bonne méthode?
    Il n'y a pas beaucoup d'infos Mais comme tu maîtrises l'entrée tu connais la sortie.
    Donc passer par un ficher me semble superflu : tu n'appelles pas write et tu retournes carrément ce que tu dois écrire et donc tester une bonne chaîne de caractères.
    Mais cela nécessite de séparer l'écriture de l'affichage : à voir si c'est intéressant.


    Citation Envoyé par MadiPoupou Voir le message
    Et lorsque l'on souhaite tester une fonction qui efface de la mémoire. Comment peut-on vérifier qu'elle fait bien son travail? Parce qu'en plus, cette dernière est supposée mettre à NULL le pointeur qui servait à identifier la zone mémoire... Je peux vérifier si ce dernier est bien NULL, mais la zone mémoire...
    En réalité, malloc, free, delete, et autres sont des opérateurs que tu peux surcharger ... mais c'est un truc de rexoor de la mort
    Il faut chercher une petite bibliothèque qui traque les fuites mémoire pour un exemple

    Édit 1 : Évidemment surcharger les opérateurs ne garanti pas que la mémoire soit libérée mais au moins qu'il n'y a pas de fuite de mémoire. Ensuite, en C, il y a une méthode pour avoir la quantité de mémoire utilisée (mais elle n'est pas portable il me semble)

    Pour l'EDI Visual Studio, tu as un petit plugin Visual Leak Detector. Pour les autres EDIs, il faut chercher.
    Sous Linux, l'outil c'est Valgrind.

    Édit : En C, on fait comme cela:
    On va redéfinir les noms des opérateurs avec un define en utilisant une fonction avec la même signature.
    Et cette fonction de remplacement, va appeler le bon opérateur (*) mais va en plus soit afficher des traces soit compter les allocations/ désallocations soit autre chose
    Évidement, des variables globales vont être utilisées : donc attention
    * : surcharger ces opérateurs permet aussi de les remplacer par, par exemple, une machine virtuelle

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #define  malloc(size) 	    	   	xmalloc (size, __FILE__, __LINE__)
    #define  calloc(elements, size)  	xcalloc (elements, size, __FILE__, __LINE__)
     
    /** replacement of malloc */
    void * xmalloc(unsigned int size,
      const char * file, unsigned int line) {
      void * ptr = malloc(size);
     
      if (ptr != NULL) {
        add_mem_info(ptr, size, file, line);
      }
     
      return ptr;
    }
     
     
    /** replacement of calloc : using linked list to store allocations */
    void * xcalloc(unsigned int elements, unsigned int size,
      const char * file, unsigned int line) {
      unsigned total_size;
      void * ptr = calloc(elements, size);
     
      if (ptr != NULL) {
        total_size = elements * size;
        add_mem_info(ptr, total_size, file, line);
      }
     
      return ptr;
    }

  7. #7
    Invité
    Invité(e)
    Par défaut
    Merci encore

    Citation Envoyé par foetus Voir le message
    Il n'y a pas beaucoup d'infos Mais comme tu maîtrises l'entrée tu connais la sortie.
    Donc passer par un ficher me semble superflu : tu n'appelles pas write et tu retournes carrément ce que tu dois écrire et donc tester une bonne chaîne de caractères.
    Mais cela nécessite de séparer l'écriture de l'affichage : à voir si c'est intéressant.
    Les fonctions que je veux tester ne retournent rien (j'aurais peut-être dû dire "procédures") et, je risque d'être amené à tester des codes qui ne sont pas de moi (donc dont je ne peux pas vraiment toucher le code, enfin si, mais je voudrais éviter de perdre du temps) et qui font la même chose (écrire une chaîne de caractères sur la sortie standard, par exemple). Ce sont vraiment des exemples faciles, mais c'est toujours dans la même idée d'acquérir des compétences sur de petits projets, avant de les appliquer sur des plus gros.
    Mais du coup je comprends l'idée de travailler dans le corps de la fonction pour vérifier ce qu'elle write.

    Merci encore!

  8. #8
    Rédacteur/Modérateur


    Homme Profil pro
    Network game programmer
    Inscrit en
    Juin 2010
    Messages
    7 177
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 39
    Localisation : Canada

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 177
    Billets dans le blog
    4
    Par défaut
    Faut t'adapter à la difficulté, ce qui est en place, la façon d'éxécuter les tests etc.
    Par exemple perso je me contente de vérifier que la valeur retourneé correspond à ce que j'attends avec quelques macros simples.
    Si FAILURE apparait dans la sortie, il y a eu une erreur.
    C'est pas aussi sexy que Google test, unit test etc, mais j'ai pu mettre en place ces tests simples tout seul, très rapidement, sans avoir à apprendre un énième framework dédié ni rajouter X fichiers au projet et tous mes collègues ont compris immédiatement comment ça marche et l'utiliser.
    Pensez à consulter la FAQ ou les cours et tutoriels de la section C++.
    Un peu de programmation réseau ?
    Aucune aide via MP ne sera dispensée. Merci d'utiliser les forums prévus à cet effet.

  9. #9
    Membre Expert
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 516
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 516
    Par défaut
    Bonjour,

    À propos des tests unitaires, je vais te donner un conseil inhabituel : écris du code facile à parser afin de pouvoir le modifier facilement plus tard à l'aide d'un script.

    Par exemple, admettons que j'écrive des tests très rudimentaires avec des assert :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
     
    #include <assert.h>
    #include <stdio.h>
     
    void test_add() {
    	assert(1 + 2 == 3);
    	assert(4 + 5 == 9);
    }
     
    void test_minus() {
    	assert(10 - 1 == 9);
    	assert(1 - 10 == -9);
    }
     
    void test_all() {
    	test_add();
    	test_minus();
    }
     
    int main() {
    	test_all();
    	return 0;
    }
    Un jour, je change d'avis. Je décide que le programme doit me retourner le nombre d'erreurs rencontrées. Je veux aussi que, pour chaque erreur, une ligne soit écrite dans la sortie d'erreur pour m'aider à savoir rapidement quels tests ont échoué.

    Si mon code était très long et réparti sur plusieurs dizaines de fichiers, alors il aurait été très long à modifier à la main. Par contre, s'il suit une syntaxe très régulière, alors je peux facilement automatiser la réécriture du code.

    Par exemple, voici un script écrit en Python :
    Code python : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
     
    import os, traceback
     
    FILE_IN_PATH  = 'main_old.c'
    FILE_OUT_PATH = 'main.c'
     
    BEGIN_OF_FILE_OUT = ''\
    	+ '\n'     + '#ifdef CHECK'\
    	+ '\n'     + '# error CHECK is already defined.'\
    	+ '\n'     + '#endif'\
    	+ '\n'     + '#define CHECK(cond) \\'\
    	+ '\n\t'   +     'if((cond) == 0) { \\'\
    	+ '\n\t\t' +         '++errorCount; \\'\
    	+ '\n\t\t' +         'fprintf(stderr, "Fail: %s: %s\\n", __func__, #cond); \\'\
    	+ '\n\t'   +     '} else \\'\
    	+ '\n\t\t' +         '(void) 0'\
    	+ '\n'
     
    def _mainImpl():
    	with open(FILE_IN_PATH) as fileIn:
    		with open(FILE_OUT_PATH, 'w') as fileOut:
    			work(fileIn, fileOut)
     
    def work(fileIn, fileOut):
    	fileOut.write(BEGIN_OF_FILE_OUT)
    	for line in fileIn:
    		if line.startswith('void test'):
    			fileOut.write('int ' + line[5::])
    			fileOut.write('\tint errorCount = 0;\n')
    		elif line.startswith('int main'):
    			fileOut.write(line)
    			fileOut.write('\tint errorCount = 0;\n')
    		elif line.startswith('\tassert'):
    			fileOut.write('\tCHECK' + line[7::])
    		elif line.startswith('\ttest_'):
    			fileOut.write('\terrorCount += ' + line[1::])
    		elif line == '}\n':
    			fileOut.write('\treturn errorCount;\n}\n')
    		elif line == '\treturn 0;\n':
    			pass
    		else:
    			fileOut.write(line)
     
    if __name__ == '__main__':
    	try:
    		_mainImpl()
    	except Exception as e:
    		print('Error: ' + str(e))
    		traceback.print_exc()
    		os.system('pause')

    Ce script me permet de transformer l'ancien code en :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
     
    #ifdef CHECK
    # error CHECK is already defined.
    #endif
    #define CHECK(cond) \
    	if((cond) == 0) { \
    		++errorCount; \
    		fprintf(stderr, "Fail: %s: %s\n", __func__, #cond); \
    	} else \
    		(void) 0
     
    #include <assert.h>
    #include <stdio.h>
     
    int test_add() {
    	int errorCount = 0;
    	CHECK(1 + 2 == 3);
    	CHECK(4 + 5 == 9);
    	return errorCount;
    }
     
    int test_minus() {
    	int errorCount = 0;
    	CHECK(10 - 1 == 9);
    	CHECK(1 - 10 == -9);
    	return errorCount;
    }
     
    int test_all() {
    	int errorCount = 0;
    	errorCount += test_add();
    	errorCount += test_minus();
    	return errorCount;
    }
     
    int main() {
    	int errorCount = 0;
    	errorCount += test_all();
    	return errorCount;
    }
    Écrire un script Python si court n'est possible que si le code C d'origine suit une certaine régularité.

    Dans mon exemple, l'intérêt n'est pas flagrant. Mais avec un projet de tests unitaires qui tient sur une centaine de fichiers, on est bien content quand on peut automatiser la réécriture du code !

    Cette remarque s'applique aussi quand on veut pouvoir passer facilement d'un framework de tests unitaires à un autre.

    Au passage, j'ajoute un deuxième conseil : contrairement aux codes présentés par foetus et Bousk, quand un test réussit, ce n'est pas forcément une bonne idée d'écrire à chaque fois une ligne de texte avec "Success" ou "[OK]". En effet, tant que tu n'as que quelques tests, ça ce n'est pas trop gênant. Mais quand tu en as plusieurs centaines voire plusieurs milliers, c'est hyper lourd.
    Cela dit, si tu passes par une macro comme te l'a conseillé Bousk, alors tu peux facilement changer la définition de la macro pour n'écrire une ligne de texte que quand le test échoue.

  10. #10
    Invité
    Invité(e)
    Par défaut
    Merci beaucoup pour vos conseils.
    Je dois dire que, pour mon projet, j'ai écrit des tests peu "modulables". Enfin les idées le sont, mais le code mériterait d'être un petit peu plus soigné et structuré. Je crois que j'aurai bientôt un projet de développement de ma propre "librairie" de tests unitaires, ce sera l'occasion de mieux organiser tout ça.

    Malgré tout, faire cet effort pour la première fois m'a vraiment aidé à comprendre quelques trucs intéressants. Je me suis notamment rendu compte du fait qu'assez instinctivement, on vient à écrire les tests avant même d'avoir codé le module à tester. On est aussi obligé de s'interroger sur l'objectif final, à mieux rechercher les cas d'erreur, etc.
    Bref, tester quoi...

    Je constate aussi que les macros ont un intérêt fort en C. Je crois que j'ai encore beaucoup de boulot pour maîtriser convenablement ce langage Et je me dis aussi que lorsqu'on doit tester et debugger de gros projets, il doit y avoir beaucoup de travail.

    D'ailleurs, pour des projets dont les modules sont très "interdépendants", est-ce que vous utilisez plusieurs "couches" de tests? Par exemple, des testes unitaires pour chaque fonction en nécessitant, puis des tests plus globaux de fonctionnement, etc ? En fait, comme le suggère un article sur le site daté de quelques jours, la recherche de failles dans les programmes semble être un enjeu colossal du boulot de développeur. J'imagine que c'est une lapalissade pour des dev. chevronnés, mais quand on débute, ça surprend un peu.

  11. #11
    Membre Expert
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 516
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 516
    Par défaut
    De mon côté, ce n'est pas pour toutes les fonctions que j'écris des tests, pour plusieurs raisons.

    1. Il vaut parfois mieux ne tester que les fonctions appelantes.

    Je vais m'appuyer sur un exemple : un jour, en C++, j'ai écrit un convertisseur de nombre en chaîne (disponible ici). Par exemple, si on lui donne le nombre 42, alors le convertisseur retourne u8"quarante-deux". Pour ce convertisseur, j'ai écrit plein de tests unitaires.

    Ce convertisseur appelle indirectement une sous-fonction chaineNombreEntre1Et999 capable de convertir un nombre entre 1 et 999 en chaîne. Cette sous-fonction appelle elle-même une sous-fonction chaineNombreEntre1Et99. Volontairement, dans mes tests unitaires, je n'ai appelé directement ni chaineNombreEntre1Et999, ni chaineNombreEntre1Et99. Je n'ai appelé que le convertisseur de plus haut niveau.

    La raison est simple : si le convertisseur reste une boîte noire pour ma suite de tests unitaires, alors je peux réorganiser comme je veux le code à l'intérieur sans avoir besoin de mettre à jour le code des tests unitaires. Cependant, lors de mes tests, je me suis quand même arrangé pour avoir une couverture globale de mon convertisseur : tous les chemins d'exécution des sous-fonctions sont tous testés indirectement par le code qui teste le convertisseur.

    2. Certaines fonctions sont trop simples pour nécessiter des tests unitaires.

    Par exemple, quand une fonction ne fait qu'une ligne de code et n'appelle aucune opération complexe non testée, ce n'est pas la peine de la blinder de tests unitaires par dessus.

    3. Certains codes auraient besoin de tests unitaires, mais sont trop mal écrits pour qu'on puisse facilement automatiser les tests.

    Un programme bien écrit devrait être découpé en briques dont la majorité sont réutilisables et faciles à tester. Mais, en ce moment, je travaille sur un code qui a une architecture de type Grande boule de boue. Du coup, si j'écrivais des tests unitaires sur des morceaux de cette boue, ces tests seraient longs à écrire et auraient trop souvent besoin d'être réécrits. Du coup, je préfère contentrer mes tests unitaires sur les quelques morceaux de code isolés proprement du reste. Le reste, j'attends de l'avoir réarchitecturé avant d'écrire des tests unitaires dessus.

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Apprendre à déboguer ses programmes
    Par LittleWhite dans le forum Débuter
    Réponses: 0
    Dernier message: 11/03/2015, 21h28
  2. Besoin de conseils pour un programme
    Par snips67 dans le forum VB 6 et antérieur
    Réponses: 1
    Dernier message: 23/05/2009, 11h59
  3. Besoin de conseils sur un programme
    Par Lynx91 dans le forum C
    Réponses: 11
    Dernier message: 12/09/2007, 13h36
  4. besoin de conseil pour apprendre l'infographie
    Par maltutoma dans le forum Etudes
    Réponses: 9
    Dernier message: 11/09/2007, 11h41
  5. besoin de conseil création programme (débutante)
    Par kwakly dans le forum WinDev
    Réponses: 1
    Dernier message: 16/05/2007, 13h19

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo