Webinaire en vedette : Dévoilement de Parasoft C/C++test CT pour l'excellence en matière de tests continus et de conformité | Voir le séminaire

Quand se moquer du code C/C++ des tests unitaires

Portrait de Miroslaw Zielinski, directeur de la gestion des produits chez Parasoft
Le 2 juin 2023
7 min lire

Le test unitaire est le processus de séparation des unités et d'exécution de tests indépendants sur chacune d'elles. Cet article comprend des conseils complets sur le moment où se moquer des tests unitaires et quelques conseils utiles pour les tests unitaires C et C++.

Tests unitaires consiste à tester des unités ou des fonctions et opérations isolées. Dans cet article, nous examinons diverses possibilités de moquerie, notamment en abordant certaines questions courantes pour vous guider efficacement Tests unitaires C et C++.

En partant d'un exemple, je reçois souvent la question : "De combien d'isolement de test unitaire avons-nous besoin ?" Il s'agit d'une question récurrente et importante souvent débattue lors du développement de tests unitaires pour C et C++.

Je ne parle pas ici de l'isolement du collègue développeur assis à côté de nous dans l'espace ouvert et tambourinant le rythme de la musique de ses écouteurs, ce qui, soit dit en passant, est également très important lorsque nous voulons créer de bonne qualité code. Je parle de l'isolement du code testé de son environnement environnant - ses soi-disant collaborateurs.

Avant de continuer, permettez-moi de clarifier une chose. Lorsque l'on parle de stubbing et de mocking pour les langages C et C++, il y a généralement une ligne tracée entre C et C++ en raison des différences dans la couche de langage reflétées dans la complexité, les capacités et les attentes concernant les frameworks de mocking typiques.

Avec Parasoft C/C++test, la situation est légèrement différente car la plupart des fonctionnalités du framework sont disponibles pour les deux langages. Ainsi, lors de la discussion de ce sujet, je donnerai un exemple de test unitaire C ou C++, et à moins que je ne marque spécifiquement quelque chose comme pris en charge uniquement pour C ou C++, vous devez toujours supposer que des fonctionnalités spécifiques sont fournies pour les deux langages.

Qu'est-ce que la moquerie et comment ça marche dans les tests ?

Les définitions sont connues pour être incohérentes et ont causé une certaine confusion. Par conséquent, cela mérite une explication rapide de ce qu'est un talon. Ensuite, expliquer ce qu'est une simulation est plus facile.

Le but des deux est de remplacer un morceau de code ou une dépendance à l'extérieur de l'unité. L'élimination de toutes les dépendances d'une unité ou d'une fonction permet à vos tests de se concentrer sur le test de la qualité (sûreté, sécurité et fiabilité) de l'unité.

Le stub remplace les dépendances, mais c'est une implémentation simple qui renvoie des valeurs prédéfinies. Les simulations se concentrent sur le comportement, elles constituent donc une implémentation contrôlée par le test unitaire. Ils peuvent être implémentés avec des valeurs de retour, vérifier les valeurs des arguments et aider à vérifier la fonctionnalité des exigences de sécurité communes. Cependant, en toute honnêteté, lorsque je crée mes cas de tests unitaires, je ne me demande pas si je suis en train d'écraser ou de me moquer. Je me concentre uniquement sur le test de la fonctionnalité pour déterminer si elle répond à mes exigences et si la mise en œuvre est robuste.

Isoler ou ne pas isoler ?

Pour certaines personnes, le bon sens dicte de ne pas s'isoler à moins d'avoir une bonne raison de le faire. Tester en incluant d'autres collaborateurs ou fonctionnalités ne fait qu'augmenter notre pénétration de la base de code. Et pourquoi devrions-nous laisser passer l'opportunité d'obtenir une couverture de code supplémentaire et la possibilité de trouver des bogues en dehors de l'unité ? Eh bien, il y a de bonnes raisons à cela.

Un testeur d'unité orthodoxe dira que les tests unitaires consistent à tester les unités isolées et ça devrait rester comme ça. Si chaque unité individuelle est saine, elle ne fait que renforcer l'ensemble. En outre, tester avec de vrais collaborateurs ou inclure d'autres fonctions dans le test est appelé test d'intégration. Si nous incluons tout le code, les cas de test sont appelés tests système.

Il existe différents niveaux d'abstraction. Les développeurs et les testeurs doivent effectuer des tests à ces différents niveaux. Le niveau le plus bas est appelé test unitaire et vous devez effectuer l'isolation de l'unité. Passons en revue quelques raisons de se moquer et les meilleures pratiques pour isoler les unités.

Raisons de se moquer des unités dans votre processus de test

1. Collaborateur pas encore implémenté ou encore en cours de développement

C'est simple. Nous n'avons pas le choix et nous avons besoin d'une mise en œuvre fictive. Le diagramme ci-dessous illustre cet environnement de test unitaire typique (SUT - système sous test, composant dépendant du DOC / collaborateur):

2. Indépendance matérielle

Pour les développeurs qui écrivent des applications de bureau, cette classe de problèmes peut sembler lointaine. Mais pour les développeurs embarqués, l'indépendance matérielle des tests unitaires est un aspect important qui permet une automatisation et une exécution de test de haut niveau sans avoir besoin de matériel.

Un bon exemple ici serait une unité sous test interagissant avec le matériel GPS, s'attendant à ce qu'une certaine séquence de coordonnées de localisation soit fournie pour calculer la vitesse. Bien que ce soit une bonne idée que nous nous entraînions davantage, je ne peux pas imaginer que des testeurs courent avec un appareil afin de simuler un mouvement, juste pour générer les entrées de test requises, chaque fois qu'une session de test unitaire est requise. À cette fin, cet exemple illustre à quel point le test GPS du cycle de développement d'un appareil serait tardif si l'indépendance matérielle n'était pas possible pendant le développement.

3. Injection de fautes

Injecter des erreurs intentionnellement est un scénario courant dans les tests. Cela peut être utilisé, par exemple, pour tester si l'allocation de mémoire a échoué ou pour voir si un composant matériel a échoué. Certains développeurs essaient de stimuler le véritable collaborateur dans la phase d'initialisation du test afin qu'il réponde par une erreur lorsqu'il est appelé depuis le code testé. Pour moi, ce n'est pas pratique et c'est généralement trop compliqué. Une fausse implémentation spécifique à un test qui simule une erreur est généralement un bien meilleur choix.

Meilleures pratiques pour la mise en œuvre de mocking dans votre processus de test

Outre ces cas évidents où un collaborateur simulé est toujours souhaité, il existe d'autres situations plus subtiles ou meilleures pratiques où se moquer ou ajouter de faux collaborateurs est un bon choix. De plus, si votre processus de test souffre de l'un des problèmes répertoriés ci-dessous, cela indique qu'une meilleure isolation du code testé est nécessaire.

1. Lorsque les tests unitaires ne sont pas reproductibles

La fugacité peut être un problème qui rend difficile la mise en œuvre de tests stables. Il existe des cas où les unités dépendent d'un signal externe pour indiquer un comportement. Un exemple classique est une unité qui s'appuie sur l'horloge système pour son comportement. Par exemple, si une unité réagit différemment à certains moments, l'automatisation est difficile à réaliser. Une bonne pratique consiste à simuler l'appel à l'horloge système et à obtenir un contrôle total sur les valeurs d'entrée de temps.

2. Lorsque les environnements de test sont difficiles à initialiser

L'initialisation de l'environnement de test peut être très complexe. Simuler les vrais collaborateurs afin qu'ils fournissent des entrées fiables au code testé peut être une tâche ardue, voire impossible.

Les composants sont souvent interdépendants. En essayant d'initialiser un module spécifique, nous pouvons finir par initialiser la moitié du système. Remplacer les vrais collaborateurs par une implémentation simulée ou factice réduit la complexité de l'initialisation de l'environnement de test.

3. Lorsque le statut du test est difficile à déterminer

Dans de nombreux cas, la détermination du verdict du test nécessite de vérifier l'état du collaborateur après l'exécution du test. Avec de vrais collaborateurs, c'est souvent impossible car il n'y a pas de méthode d'accès adaptée dans l'interface réelle du collaborateur pour interroger l'état après le test.

Remplacer un vrai collaborateur par un faux résout généralement le problème. Nous pouvons étendre la fausse implémentation avec toutes sortes de méthodes d'accès pour déterminer le résultat du test.

4. Lorsque les tests sont lents

Il y a des cas où une réponse du vrai collaborateur peut prendre un temps considérable. Il n'est pas toujours clair quand le retard devient inacceptable et quand l'isolement est nécessaire. Un retard de deux minutes dans un test est-il acceptable ou non ?

Il est souvent souhaitable de pouvoir exécuter des suites de tests aussi rapidement que possible, peut-être après chaque changement de code. Des retards importants dus à des interactions avec de vrais collaborateurs peuvent rendre les suites de tests trop lentes pour être pratiques. Les simulations de ces collaborateurs réels peuvent être plus rapides de plusieurs ordres de grandeur et ramener le temps d'exécution des tests à un niveau acceptable.

Exemple pratique de quand se moquer du code C/C++ de test unitaire

Les interfaces moqueuses peuvent rendre le travail de test beaucoup plus facile. Au lieu que votre unité appelle les autres, elle peut appeler une interface fictive. Votre code de test peut s'interposer sur tous les côtés de l'unité que vous souhaitez tester, puis en examinant toutes les sorties et en gérant toutes les entrées.

Disons que dans l'exemple de code suivant, nous voulons tester l'unité/fonction bar et lui faire appeler une fonction fake/mock foo. Pour ce faire, nous avons besoin d'un moyen de remplacer et de contrôler l'appel à foo(). Il existe plusieurs approches moqueuses pour ce faire et Parasoft peut automatiser une grande partie de cela pour vous. Dans cet exemple et pour des raisons de simplicité, une macro (#define FOO) est utilisée pour contrôler le collaborateur foo().

#ifdef TEST
#define FOO mock_foo
#else
#define FOO foo
#endif
 
int mock_foo(int x)
{
	return x;
}
 
int bar(int x)
{
	int result = 0;
	for (int i = 0; i < 10; i++)
	{
		result += FOO(i + x);
	}
	return result;
}

Questions utiles pour déterminer s'il faut ou non se moquer

Lorsque vous écrivez un nouveau test unitaire C ou C++ et que vous décidez d'utiliser des collaborateurs originaux ou des implémentations simulées, tenez compte des quatre questions suivantes.

  1. Le vrai collaborateur est-il une source de risque pour la stabilité de mes tests?
  2. Est-il difficile d'initialiser le vrai collaborateur?
  3. Est-il possible de vérifier l'état du collaborateur après le test, de décider du statut du test?
  4. Combien de temps faudra-t-il au collaborateur pour répondre?

Si nous connaissons suffisamment bien le collaborateur pour répondre à toutes ces questions, alors c'est une décision facile dans un sens ou dans l'autre. Si ce n'est pas le cas, je suggérerais de commencer par le vrai collaborateur et d'essayer de répondre aux quatre questions au fur et à mesure. En pratique, c'est l'approche que la plupart des praticiens du développement piloté par les tests (TDD) appliquent dans leur travail quotidien. Cela signifie que vous devez prendre soin de vos cas de test et les examiner attentivement jusqu'à ce qu'ils deviennent stables.

Le plus souvent, l'isolation des tests unitaires est compliquée par les dépendances de l'unité testée. Il existe des cas clairement souhaitables où se moquer d'un composant dépendant est nécessaire, mais aussi des situations plus subtiles. Dans certains cas, ce n'est pas clair et dépend du risque et de l'incertitude qu'une dépendance a dans l'environnement de test.

Comment rationaliser les tests unitaires pour les systèmes embarqués et critiques pour la sécurité