Bonjour encore! Il reste moins d'une semaine avant le début des cours en groupe au cours
"Développeur C ++" . À cet égard, nous continuons à partager du matériel utile traduit spécifiquement pour les étudiants de ce cours.

Les tests unitaires de votre code avec des modèles se rappellent de temps en temps. (Vous testez vos modèles, non?) Certains modèles sont faciles à tester. Certains ne le sont pas. Parfois, il y a un manque de clarté ultime concernant l'implémentation du faux code (stub) dans le modèle testé. J'ai observé plusieurs raisons pour lesquelles l'intégration de code devient compliquée.
Ci-dessous, j'ai donné quelques exemples avec une complexité à peu près croissante de la mise en œuvre du code.
- Le modèle prend un argument de type et un objet du même type par référence dans le constructeur.
- Le modèle prend un argument de type. Fait une copie de l'argument constructeur ou ne l'accepte tout simplement pas.
- Un modèle prend un argument de type et crée plusieurs modèles interconnectés sans fonctions virtuelles.
Commençons par un simple.
Le modèle prend un argument de type et un objet du même type par référence dans le constructeur
Ce cas semble simple, car le test unitaire crée simplement une instance du modèle de test avec le type de stub. Certaines instructions peuvent être vérifiées pour la classe fictive. Et c'est tout.
Naturellement, les tests avec un seul argument de type ne disent rien sur le reste du nombre infini de types qui peuvent être passés au modèle. Une manière élégante de dire la même chose: les motifs sont reliés par un quantificateur de généralité, il nous faudra donc peut-être devenir un peu plus perspicaces pour des tests plus scientifiques. Plus d'informations à ce sujet plus tard.
Par exemple:
template <class T> class TemplateUnderTest { T *t_; public: TemplateUnderTest(T *t) : t_(t) {} void SomeMethod() { t->DoSomething(); t->DoSomeOtherThing(); } }; struct MockT { void DoSomething() {
Le modèle prend un argument de type. Fait une copie de l'argument constructeur ou ne l'accepte tout simplement pas
Dans ce cas, l'accès à l'objet à l'intérieur du modèle peut ne pas être possible en raison des droits d'accès. Vous pouvez utiliser des classes d'
friend
.
template <class T> class TemplateUnderTest { T t_; friend class UnitTest; public: void SomeMethod() { t.DoSomething(); t.DoSomeOtherThing(); } }; class UnitTest { void Test2() { TemplateUnderTest<MockT> test; test.SomeMethod(); assert(DoSomethingWasCalled(test.t_));
UnitTest :: Test2
a accès au corps de TemplateUnderTest et peut vérifier les instructions sur la copie interne de MockT.
Un modèle prend un argument de type et crée plusieurs modèles interconnectés sans fonctions virtuelles
Pour ce cas, je considérerai un exemple réel:
Google RPC asynchrone .
En C ++, async gRPC a quelque chose appelé CallData, qui, comme son nom l'indique, stocke les
données liées à un appel RPC . Le modèle CallData peut gérer plusieurs types de RPC différents. Il est donc naturel qu'il soit implémenté précisément par le modèle.
Un CallData générique accepte deux arguments de type: Request et Response. Cela peut ressembler à ceci:
template <class Request, class Response> class CallData { grpc::ServerCompletionQueue *cq_; grpc::ServerContext context_; grpc::ServerAsyncResponseWriter<Response> responder_;
Le test unitaire du modèle CallData doit vérifier le comportement de HandleRequest et HandleResponse. Ces fonctions invoquent un certain nombre de fonctions membres. Par conséquent, la vérification de l'intégrité de leur appel est primordiale pour l'intégrité de CallData. Cependant, il existe des astuces.
- Certains types de l'espace de noms grpc sont créés en interne et ne sont pas transmis par le constructeur.
ServerAsyncResponseWriter
et ServerAsyncResponseWriter
, par exemple. grpc :: ServerCompletionQueue
est passé au constructeur comme argument, mais n'a pas de fonctions virtuelles. Seul un destructeur virtuel.grpc :: ServerContext
est créé en interne et n'a aucune fonction virtuelle.
La question est de savoir comment tester CallData sans utiliser le gRPC complet dans les tests? Comment simuler ServerCompletionQueue? Comment simuler ServerAsyncResponseWriter, qui est lui-même un modèle? et ainsi de suite ...
Sans fonctions virtuelles, remplacer le comportement de l'utilisateur devient une tâche intimidante. Les types codés en dur, tels que grpc :: ServerAsyncResponseWriter, ne peuvent pas être modélisés car ils le sont, hmm, codés en dur et non implémentés.
Il ne sert à rien de les passer comme arguments constructeurs. Même si vous faites cela, cela peut ne pas avoir de sens, car il peut s'agir de classes finales ou simplement de fonctions virtuelles.
Alors on fait quoi?
Solution: Traits

Au lieu d'incorporer un comportement personnalisé en héritant d'un type générique (comme cela se fait dans la programmation orientée objet), INSÉRER LE TYPE. Nous utilisons des traits pour cela. Nous nous spécialisons dans les traits de différentes manières selon le type de code qu'il s'agit: un code de production ou un code de test unitaire.
Considérez
CallDataTraits
template <class CallData> class CallDataTraits { using ServerCompletionQueue = grpc::ServerCompletionQueue; using ServerContext = grpc::ServerContext; using ServerAsyncResponseWriter = grpc::ServerAsyncResponseWrite<typename CallData::ResponseType>; };
Il s'agit du modèle principal de trait utilisé pour le code de production. Utilisons-le dans un CallDatatemplate.
En regardant le code ci-dessus, il est clair que le code d'application utilise toujours des types de l'espace de noms grpc. Cependant, nous pouvons facilement remplacer les types grpc par des types factices. Voir ci-dessous.
Les traits nous ont permis de choisir les types implémentés dans CallData, selon la situation. Cette méthode ne nécessite pas de performances supplémentaires, car aucune fonction virtuelle inutile n'a été créée pour ajouter des fonctionnalités. Cette technique peut également être utilisée dans les classes finales.
Comment aimez-vous le matériel? Écrivez des commentaires. Et à la porte ouverte ;-)