Tester un membre de classe privé en C++ sans ami [dupliquer]
Cette question a déjà une réponse ici:
Aujourd'hui, j'ai eu une discussion avec un collègue sur la question de savoir s'il fallait tester ou non les députés ou l'état privé dans la classe. Il a presque convaincu moi pourquoi cela a du sens. Cette question ne vise pas à dupliquer des questions déjà existantes sur la nature et la raison de tester des membres privés, comme: Qu'y a-t-il de mal à faire d'un test unitaire un ami de la classe qu'il teste?
collègues la suggestion était, à mon avis, Un peu fragile pour présenter la déclaration d'ami à la classe de mise en oeuvre des tests unitaires. À mon avis, c'est un non-go, parce que nous introduisons une certaine dépendance du code testé au code de test, alors que le test le code dépend déjà du code testé => dépendance cyclique. Même des choses aussi innocentes, comme renommer une classe de test, entraînent la rupture des tests unitaires et renforcent les changements de code dans le code testé.
je voudrais demander aux gourous C++ de juger sur l'autre proposition, qui repose sur le fait que nous sommes autorisés à spécialiser une fonction de modèle. Imaginez la classe:
// tested_class.h
struct tested_class
{
tested_class(int i) : i_(i) {}
//some function which do complex things with i
// and sometimes return a result
private:
int i_;
};
je n'aime pas l'idée d'avoir un getter pour i_ juste pour rendre testable. Donc ma proposition est "test_backdoor' déclaration de modèle de fonction dans la classe:
// tested_class.h
struct tested_class
{
explicit
tested_class(int i=0) : i_(i) {}
template<class Ctx>
static void test_backdoor(Ctx& ctx);
//some function which do complex things with i
// and sometimes return a result
private:
int i_;
};
en ajoutant seulement cette fonction nous pouvons rendre testables les membres privés de la classe. Remarque: Il n'y a pas de dépendance aux classes de test unitaires, ni de mise en œuvre de la fonction de modèle. Dans cet exemple, l'implémentation de test unitaire utilise le cadre de test Boost.
// tested_class_test.cpp
namespace
{
struct ctor_test_context
{
tested_class& tc_;
int expected_i;
};
}
// specialize the template member to do the rest of the test
template<>
void tested_class::test_backdoor<ctor_test_context>(ctor_test_context& ctx)
{
BOOST_REQUIRE_EQUAL(ctx.expected_i, tc_.i_);
}
BOOST_AUTO_TEST_CASE(tested_class_default_ctor)
{
tested_class tc;
ctor_test_context ctx = { tc, 0 };
tested_class::test_backdoor(ctx);
}
BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
tested_class tc(-5);
ctor_test_context ctx = { tc, -5 };
tested_class::test_backdoor(ctx);
}
en introduisant juste un modèle de déclaration unique, qui n'est pas du tout appelable, nous donnons à l'exécuteur du test la possibilité de transférer la logique du test dans un fonction. La fonction, qui agit sur les contextes de type safe, n'est visible que de l'intérieur de l'Unité de compilation de test, en raison de la nature de type anonyme du contexte de test. Et la meilleure chose est que nous pouvons définir autant de contextes de test anonymes que nous le voulons et les spécialiser sur eux, sans jamais toucher à la classe testée.
bien sûr, les utilisateurs doivent savoir ce qu'est la spécialisation template, mais est-ce que ce code est vraiment mauvais ou bizarre ou illisible? Ou Puis-je m'attendre à ce que les développeurs C++ connaissance qu'est-ce que la spécialisation C++ et comment cela fonctionne?
développement sur l'utilisation d'ami pour déclarer la classe de test unité Je ne pense pas que ce soit robuste. Imaginez boost framework (ou peut-être d'autres cadres de test). Il génère pour chaque cas d'essai un type séparé. Mais pourquoi devrais-je m'intéresser aussi longtemps que je peux écrire:
BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
...
}
si j'utilisais des amis, je devais déclarer chaque cas d'essai comme un ami alors... Ou finir par introduire une certaine fonctionnalité de test dans un type commun (comme fixture), déclarez-le comme un ami, et envoyez tous les appels de test à ce type... N'est-ce pas bizarre?
j'aimerais voir vos POUR et contre pratiquer cette approche.
9 réponses
ce qui va suivre n'est pas techniquement parlant une réponse directe à votre question comme il fera toujours usage de la fonctionnalité "ami" mais il ne nécessite pas de modification de l'entité testée elle-même. et je pense qu'il addesses le souci de briser l'encapsulation mentionné dans certaines des autres réponses; il ne nécessite écrire du code réutilisable.
L'idée derrière, c'est pas la mienne, et la mise en œuvre est entièrement basé sur un truc présentés et expliqué par litb sur son blog(couplé avec ce Sutter est gotw juste un petit peu plus de contexte, au moins pour moi) - EN BREF CRTP, amis, ADL et des conseils aux membres (Je dois avouer qu'à mon grand désarroi, la partie ADL, Je n'ai toujours pas obtenez-le entièrement, mais je travaille sans relâche à comprendre 100%).
Je l'ai testé avec les compilateurs gcc 4.6, clang 3.1 et VS2010 et it fonctionne parfaitement.
/* test_tag.h */
#ifndef TEST_TAG_H_INCLUDED_
#define TEST_TAG_H_INCLUDED_
template <typename Tag, typename Tag::type M>
struct Rob
{
friend typename Tag::type get(Tag)
{
return M;
}
};
template <typename Tag, typename Member>
struct TagBase
{
typedef Member type;
friend type get(Tag);
};
#endif /* TEST_TAG_H_INCLUDED_ */
/* tested_class.h */
#ifndef TESTED_CLASS_H_INCLUDED_
#define TESTED_CLASS_H_INCLUDED_
#include <string>
struct tested_class
{
tested_class(int i, const char* descr) : i_(i), descr_(descr) { }
private:
int i_;
std::string descr_;
};
/* with or without the macros or even in a different file */
# ifdef TESTING_ENABLED
# include "test_tag.h"
struct tested_class_i : TagBase<tested_class_i, int tested_class::*> { };
struct tested_class_descr : TagBase<tested_class_descr, const std::string tested_class::*> { };
template struct Rob<tested_class_i, &tested_class::i_>;
template struct Rob<tested_class_descr, &tested_class::descr_>;
# endif
#endif /* TESTED_CLASS_H_INCLUDED_ */
/* test_access.cpp */
#include "tested_class.h"
#include <cstdlib>
#include <iostream>
#include <sstream>
#define STRINGIZE0(text) #text
#define STRINGIZE(text) STRINGIZE0(text)
int assert_handler(const char* expr, const char* theFile, int theLine)
{
std::stringstream message;
message << "Assertion " << expr << " failed in " << theFile << " at line " << theLine;
message << "." << std::endl;
std::cerr << message.str();
return 1;
}
#define ASSERT_HALT() exit(__LINE__)
#define ASSERT_EQUALS(lhs, rhs) ((void)(!((lhs) == (rhs)) && assert_handler(STRINGIZE((lhs == rhs)), __FILE__, __LINE__) && (ASSERT_HALT(), 1)))
int main()
{
tested_class foo(35, "Some foo!");
// the bind pointer to member by object reference could
// be further wrapped in some "nice" macros
std::cout << " Class guts: " << foo.*get(tested_class_i()) << " - " << foo.*get(tested_class_descr()) << std::endl;
ASSERT_EQUALS(35, foo.*get(tested_class_i()));
ASSERT_EQUALS("Some foo!", foo.*get(tested_class_descr()));
ASSERT_EQUALS(80, foo.*get(tested_class_i()));
return 0;
}
je pense que le test unitaire est sur le test de l' observables comportement de la classe à l'essai. Par conséquent, il n'est pas nécessaire de tester les parties privées car elles ne sont pas elles-mêmes observables. La façon dont vous le testez est en testant si l'objet se comporte comme vous l'attendez (ce qui implique implicitement que tous les états internes privés sont dans l'ordre).
la raison pour ne pas s'inquiéter des parties privées est que de cette façon vous pouvez changer l'implémentation (par ex. refactoring), sans avoir à réécrire vos tests.
ma réponse est donc de ne pas le faire (même si cela est techniquement possible) car cela va à l'encontre de la philosophie des tests unitaires.
Pros
- Vous pouvez accéder aux membres privés de les tester
- C'est une quantité assez minime de
hack
Cons
- Cassé encapsulation
- encapsulation cassée qui est plus compliquée et aussi fragile que
friend
- mélanger le test avec le code de production en mettant
test_backdoor
du côté de la production - problème de Maintance ( tout comme amis du le code de test, vous avez créé un extrêmement couplage étroit avec votre code de test )
tous les avantages et les inconvénients mis à part, je pense que vous êtes mieux de faire quelques changements d'architecture qui permettent de mieux tester toutes les choses complexes qui se produisent.
Solutions Possibles
- utilisez l'idiome Pimpl, mettez le
complex
coder dans le code pimpl avec le membre privé, et écrire un test pour le Pimpl. Le Pimpl peut être avancé en tant que membre public, permettant l'instanciation externe dans le test unitaire. Le Pimpl ne peut être composé que de membres publics, ce qui le rend plus facile à tester- désavantage: beaucoup de code
- inconvénient: type opaque qui peut être plus difficile à voir à l'intérieur du débogage
- il suffit de tester l'interface publique/protégée de la classe. Tester le contrat que votre interface établit hors.
- désavantage: les tests unitaires sont difficiles / impossibles à écrire de façon isolée.
- similaire aux solutions Pimpl, mais créer une fonction libre avec le
complex
dans le code. Mettez la déclaration dans un en-tête privé ( qui ne fait pas partie de l'interface publique des bibliothèques ), et vérifiez-la. - encapsulation par l'intermédiaire d'un ami méthode d'essai / montage
- variation Possible sur ceci: declare
friend struct test_context;
, mettez votre code de test à l'intérieur des méthodes dans la mise en oeuvre destruct test_context
. De cette façon, vous n'avez pas à l'ami chaque cas de test, méthode, ou d'installation. Cela devrait réduire la probabilité que quelqu'un brise l'amitié.
- variation Possible sur ceci: declare
- Briser l'encapsulation via la spécialisation de modèle
tester des membres privés ne consiste pas toujours à vérifier l'état en vérifiant s'il est égal à certaines valeurs attendues. Afin d'accommoder d'autres scénarios de test plus complexes, j'utilise parfois l'approche suivante (simplifiée ici pour transmettre l'idée principale):
// Public header
struct IFoo
{
public:
virtual ~IFoo() { }
virtual void DoSomething() = 0;
};
std::shared_ptr<IFoo> CreateFoo();
// Private test header
struct IFooInternal : public IFoo
{
public:
virtual ~IFooInternal() { }
virtual void DoSomethingPrivate() = 0;
};
// Implementation header
class Foo : public IFooInternal
{
public:
virtual DoSomething();
virtual void DoSomethingPrivate();
};
// Test code
std::shared_ptr<IFooInternal> p =
std::dynamic_pointer_cast<IFooInternal>(CreateFoo());
p->DoSomethingPrivate();
cette approche a le net avantage de promouvoir un bon design et de ne pas être brouillon avec les déclarations d'amis. Bien sûr, vous ne devez pas passer par la difficulté la plupart du temps parce que d'être en mesure de tester les députés privés sont une exigence assez peu standard pour commencer.
Je ne ressens généralement pas le besoin de tester les membres privés et les fonctions de l'unité. Je préférerais introduire une fonction publique juste pour vérifier l'état interne correct.
Mais si je décide d'aller fouiller dans les détails, j'utilise un méchant rapide hack dans le programme de test unitaire:
#include <system-header>
#include <system-header>
// Include ALL system headers that test-class-header might include.
// Since this is an invasive unit test that is fiddling with internal detail
// that it probably should not, this is not a hardship.
#define private public
#include "test-class-header.hpp"
...
sur Linux au moins cela fonctionne parce que le nom c++ mangling n'inclut pas l'état privé/public. On me dit que sur d'autres systèmes, ce n'est peut-être pas le cas. ne serait pas le lien.
j'ai utilisé une fonction pour tester les membres privés de la classe qui s'appelait juste TestInvariant().
c'était un membre privé de la classe et, en mode debug, il était appelé au début et à la fin de chaque fonction (sauf le début du ctor et la fin du dctor).
c'était virtuel et n'importe quelle classe de base appelée la version parent avant la sienne.
cela m'a permis de vérifier l'état interne de la classe tout le temps sans exposer les intensités de la classe à n'importe qui. J'ai eu des tests très simples, mais il n'y a aucune raison pour que vous ne puissiez pas avoir des tests compliqués, ou même l'allumer ou l'éteindre avec un drapeau, etc.
vous pouvez aussi avoir des fonctions de Test publiques qui peuvent être appelées par d'autres classes qui appellent votre fonction TestInvariant (). Par conséquent, lorsque vous devez changer le fonctionnement de la classe interne, vous n'avez pas besoin de changer de code d'utilisateur.
Serait-il aider?
je pense que la première chose à demander est: pourquoi ami est considéré comme quelque chose qui doit être utilisé avec prudence?
parce qu'il casse l'encapsulation. Il fournit à une autre classe ou fonction l'accès aux intérieurs de votre objet, élargissant ainsi la portée visible de vos membres privés. Si vous avez beaucoup d'amis, c'est beaucoup plus difficile de raisonner sur l'état de votre objet.
à mon avis, la solution du modèle est encore pire qu'ami à cet égard. Votre principal a déclaré avantage du modèle est que vous n'avez plus besoin explicitement ami le test de la classe. Je vous dirais que, au contraire, c'est un inconvénient. Il y a deux raisons pour cela.
le test est couplé aux internes de votre classe. Toute personne changeant la classe devrait savoir qu'en changeant les parties intimes de l'objet qu'ils peuvent briser le test. ami leur dit exactement quels objets pourraient être couplés à l'état interne de votre classe, mais le modèle de solution qui ne fonctionne pas.
ami limite l'extension de la portée de vos privates. Si vous fréquentez une classe, vous savez que seule cette classe peut accéder à vos internes. Ainsi, si vous êtes un ami du test, vous savez que seul le test peut lire ou écrire à des variables de membre privé. Votre modèle de porte arrière, cependant, pourrait être utilisé n'importe où.
la solution du gabarit est inefficace parce qu'elle cache le problème plutôt que de le corriger. La question sous-jacente avec la dépendance cyclique existe encore: quelqu'un changement de la classe doit savoir au sujet de chaque utilisation de la porte, et quelqu'un le test doit savoir à propos de la classe. Fondamentalement, la référence au test de la classe a été supprimée seulement en transformant toutes les données privées en données publiques d'une manière détournée.
si vous devez accéder à des membres privés à partir de votre test, il suffit d'amitiés le montage d'essai et être fait avec. C'est simple et compréhensible.
je suis désolé de vous conseiller cela, mais cela m'a aidé quand la plupart des méthodes dans ces réponses ne sont pas réalisables sans un remaniement fort: ajouter avant l'en-tête pour le fichier avec la classe dont vous souhaitez accéder aux membres privés,
#define private public
C'est mal, mais
n'interfère pas avec le code de production
ne casse pas l'encapsulation comme ami / changer le niveau d'accès fait
évite le remaniement lourd avec PIMPL idiome
de sorte que vous pouvez aller pour elle...
Il y a une théorie que si c'est privé il ne devrait pas être testé seul, si il a besoin alors il devrait être redessiné.
Pour moi, c'est le chiisme.
sur certains projets, les gens créent une macro pour les méthodes privées, comme:
class Something{
PRIVATE:
int m_attr;
};
lors de la compilation pour le test privé est défini comme public, sinon il est défini comme privé. aussi simple que cela.