teorico programas programar programacion privados principiantes practico paso para libro intercambio ejemplos datos como clases amigas c++ unit-testing private protected members

programas - Prueba de un miembro de una clase privada en C++ sin un amigo



intercambio de datos privados de clases c++ (9)

Creo que la prueba de unidad se trata de probar el comportamiento observable de la clase bajo prueba. Por lo tanto, no es necesario probar las partes privadas, ya que ellas mismas no son observables. La forma en que lo prueba es comprobando si el objeto se comporta de la forma en que lo espera (lo que implícitamente implica que todos los estados internos privados están en orden).

La razón para no preocuparse por las partes privadas es que de esta manera puede cambiar la implementación (por ejemplo, refactorización), sin tener que volver a escribir sus pruebas.

Así que mi respuesta es no hacerlo (aunque sea técnicamente posible) ya que va en contra de la filosofía de las pruebas unitarias.

Hoy tuve una discusión con un colega sobre si probar o no probar miembros privados o estados privados en la clase. Casi me convenció de por qué tiene sentido. Esta pregunta no pretende duplicar las preguntas ya existentes de StackOverflow sobre la naturaleza y la razón de las pruebas de los miembros privados, como: ¿Qué hay de malo en hacer que una prueba de unidad sea un amigo de la clase que está probando?

En mi opinión, la sugerencia de los colegas fue un poco frágil para introducir la declaración de amigos en la clase de implementación de la prueba unitaria. En mi opinión, esto es un no-go, porque introducimos cierta dependencia del código probado al código de prueba, mientras que el código de prueba ya depende del código probado => dependencia cíclica. Incluso cosas tan inocentes como cambiar el nombre de una clase de prueba resultan en romper las pruebas unitarias e imponen cambios de código en el código probado.

Me gustaría pedir a los gurús de C ++ que juzguen sobre la otra propuesta, que se basa en el hecho de que se nos permite especializar una función de plantilla. Solo imagina la clase:

// 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_; };

No me gusta la idea de tener un captador para i_ solo para que sea comprobable. Así que mi propuesta es la declaración de la plantilla de función ''test_backdoor'' en la clase:

// 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_; };

Al agregar solo esta función podemos hacer que los miembros privados de la clase sean comprobables. Tenga en cuenta que no hay dependencia de las clases de prueba unitaria ni de la implementación de la función de plantilla. En este ejemplo, la implementación de la prueba unitaria usa el framework Boost Test.

// 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); }

Al introducir una sola declaración de plantilla, que no se puede llamar en absoluto, le damos al implementador de pruebas la posibilidad de reenviar la lógica de prueba a una función. La función actúa en contextos seguros de tipo y solo es visible desde dentro de la unidad de compilación de prueba particular, debido a la naturaleza de tipo anónimo del contexto de prueba. Y lo mejor es que podemos definir tantos contextos de prueba anónimos como nos guste y especializar pruebas en ellos, sin tocar la clase probada.

Claro, los usuarios deben saber qué es la especialización de plantillas, pero ¿es este código realmente malo o extraño o ilegible? ¿O puedo esperar de los desarrolladores de C ++ que conozcan qué es la especialización de la plantilla de C ++ y cómo funciona?

Al elaborar sobre el uso de amigos para declarar clase de prueba unitaria, no creo que esto sea robusto. Imagine boost framework (o puede ser otros frameworks de prueba). Genera para cada caso de prueba un tipo separado. Pero ¿por qué debería importarme mientras pueda escribir:

BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor) { ... }

Si utilizaba amigos, tenía que declarar cada caso de prueba como amigo, entonces ... O al final, introduzco alguna funcionalidad de prueba de algún tipo común (como un accesorio), declara que es un amigo y reenvía todas las llamadas de prueba a ese tipo ... ¿No es eso raro?

Me gustaría ver a sus pros y contras practicando este enfoque.


Creo que lo primero que debo preguntar es: ¿por qué se considera que un amigo es algo que debe usarse con precaución?

Porque rompe la encapsulación. Proporciona otra clase o función con acceso a las partes internas de su objeto, expandiendo así el alcance visible de sus miembros privados. Si tienes muchos amigos, es mucho más difícil razonar sobre el estado de tu objeto.

En mi opinión, la solución de plantilla es incluso peor que la de un amigo en ese sentido. Su principal beneficio declarado de la plantilla es que ya no necesita hacer un amigo explícito de la prueba de la clase. Sostengo que, por el contrario, esto es un perjuicio. Hay dos razones para eso.

  1. La prueba está acoplada a las partes internas de su clase. Cualquiera que cambie la clase debe saber que al cambiar los datos privados del objeto puede estar rompiendo la prueba. amigo les dice exactamente qué objetos podrían estar acoplados al estado interno de su clase, pero la solución de plantilla no lo hace.

  2. Amigo limita la expansión del alcance de sus privados. Si es amigo de una clase, sabe que solo esa clase puede acceder a sus partes internas. Por lo tanto, si es amigo de la prueba, sabe que solo la prueba puede leer o escribir en variables de miembros privados. Sin embargo, la plantilla de la puerta trasera podría usarse en cualquier lugar.

La solución de plantilla no es efectiva porque oculta el problema en lugar de solucionarlo. El problema subyacente con la dependencia cíclica todavía existe: alguien que cambia la clase debe saber acerca de cada uso de la puerta trasera, y alguien que cambia la prueba debe conocer la clase. Básicamente, la referencia a la prueba de la clase se eliminó solo al convertir todos los datos privados en datos públicos de forma indirecta.

Si debe acceder a miembros privados desde su prueba, simplemente haga amigo del dispositivo de prueba y termine con él. Es simple y comprensible.


Existe la teoría de que si es privado, no debería probarse solo, si es necesario, entonces debería ser rediseñado.

Para mi eso es el chiismo.

En algunos proyectos, las personas crean una macro para métodos privados, como:

class Something{ PRIVATE: int m_attr; };

Cuando la compilación para la prueba PRIVATE se define como pública, de lo contrario se define como privada. que simple


Lamento aconsejar esto, pero me ayudó cuando la mayoría de los métodos en esas respuestas no son alcanzables sin una refactorización fuerte: agregue antes del encabezado del archivo con la clase a los miembros privados a los que desea acceder,

#define private public

Es malvado, pero

  • no interfiere con el código de producción

  • no rompe la encapsulación como amigo / cambio de nivel de acceso hace

  • evita la refactorización pesada con el lenguaje PIMPL

así que puedes ir por ello ...


Lo que seguirá no es técnicamente una respuesta directa a su pregunta, ya que seguirá haciendo uso de la funcionalidad de "amigo", pero no requiere la modificación de la entidad probada en sí y creo que tiene la preocupación de romper la encapsulación mencionada en algunos de las otras respuestas; aunque sí requiere escribir algún código repetitivo.

La idea detrás de esto no es mía y la implementación se basa completamente en un truco presentado y explicado por litb en su blog (junto con el resumen de este Sutter para un poco más de contexto, al menos para mí), en resumen, CRTP, amigos, ADL y punteros a los miembros (Debo confesar que para mi consternación en la parte de ADL, todavía no lo entiendo por completo, pero estoy trabajando de manera irreflexiva en resolverlo al 100%).

Lo probé con los compiladores gcc 4.6, clang 3.1 y VS2010 y funciona perfectamente.

/* 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; }


No suelo sentir la necesidad de hacer pruebas unitarias de miembros y funciones privadas. Quizás prefiera introducir una función pública solo para verificar el estado interno correcto.

Pero si decido ir hurgando en los detalles, uso un truco rápido desagradable en el programa de prueba de la unidad:

#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" ...

En Linux, al menos esto funciona porque la mutilación de nombres en C ++ no incluye el estado privado / público. Me dicen que en otros sistemas esto puede no ser cierto y no se vincularía.


Probar miembros privados no siempre se trata de verificar el estado al verificar si es igual a algunos valores esperados. Para acomodar otros escenarios de prueba más complejos, a veces utilizo el siguiente enfoque (simplificado aquí para transmitir la idea principal):

// 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();

Este enfoque tiene la clara ventaja de promover un buen diseño y no ser desordenado con las declaraciones de los amigos. Por supuesto, no tiene que pasar por el problema la mayor parte del tiempo porque, para empezar, poder realizar pruebas a miembros privados es un requisito bastante no estándar.


Utilicé una función para probar miembros de clase privada que se llamaba TestInvariant ().

Era un miembro privado de la clase y, en el modo de depuración, se llamaba al principio y al final de cada función (excepto el principio del ctor y el final del dctor).

Era virtual y cualquier clase base se llamaba versión primaria antes de su propia versión.

Eso me permitió verificar el estado interno de la clase todo el tiempo sin exponer los intenales de la clase a nadie. Tuve pruebas muy simples, pero no hay ninguna razón por la que no pueda haberlas complicadas, ni siquiera activarlas o desactivarlas con una bandera, etc.

También puede tener funciones de prueba públicas a las que pueden llamar otras clases que llaman a su función TestInvariant (). Por lo tanto, cuando necesita cambiar el funcionamiento de la clase interna, no necesita cambiar ningún código de usuario.

¿Esto ayudaría?


Pros

  • Puedes acceder a los miembros privados para probarlos.
  • Es una cantidad bastante mínima de hack

Contras

  • Encapsulamiento roto
  • Encapsulación rota que es más complicada y tan frágil como la de un friend
  • Mezcla de prueba con código de producción poniendo test_backdoor en el lado de producción
  • Problema de mantenimiento (al igual que con el código de prueba, ha creado un acoplamiento extremadamente estrecho con su código de prueba)

Dejando a un lado todos los pros y los contras, creo que lo mejor es hacer algunos cambios arquitectónicos que permitan una mejor prueba de cualquier cosa compleja que esté sucediendo.

Soluciones posibles

  • Use el lenguaje Pimpl, coloque el código complex en el pimpl junto con el miembro privado y escriba una prueba para el Pimpl. El Pimpl puede ser declarado como un miembro público, lo que permite una instanciación externa en la prueba de la unidad. El Pimpl puede constar solo de miembros públicos, lo que facilita la prueba
    • Desventaja: un montón de código
    • Desventaja: tipo opaco que puede ser más difícil de ver dentro de la depuración
  • Solo prueba la interfaz pública / protegida de la clase. Probar el contrato que establece su interfaz.
    • Desventaja: las pruebas unitarias son difíciles / imposibles de escribir de manera aislada.
  • Similar a las soluciones Pimpl, pero crea una función gratuita con el código complex contiene. Coloque la declaración en un encabezado privado (que no sea parte de la interfaz pública de las bibliotecas) y pruébela.
  • Romper la encapsulación a través de un amigo un método de prueba / accesorio
    • Posible variación en esto: declarar friend struct test_context; , ponga su código de prueba dentro de los métodos en la implementación de struct test_context . De esta manera, no tiene que hacerse amigo de cada caso de prueba, método o accesorio. Esto debería reducir la probabilidad de que alguien rompa la amistad.
  • Romper la encapsulación mediante la especialización de plantillas