c++ testing pimpl-idiom

c++ - El lenguaje pImpl y la probabilidad



testing pimpl-idiom (5)

El lenguaje pImpl en c ++ apunta a ocultar los detalles de implementación (= miembros privados) de una clase de los usuarios de esa clase. Sin embargo, también oculta algunas de las dependencias de esa clase, que generalmente se consideran malas desde el punto de vista de las pruebas.

Por ejemplo, si la clase A oculta sus detalles de implementación en la clase AImpl a la que solo se puede acceder desde A.cpp y AImpl depende de muchas otras clases, es muy difícil realizar la prueba unitaria de la clase A, ya que el marco de prueba no tiene acceso a los métodos de AImpl y tampoco hay forma de inyectar dependencia en AImpl.

Alguien se ha encontrado con este problema antes? ¿Y has encontrado una solución?

editar

En un tema relacionado, parece que la gente sugiere que solo se deben probar los métodos públicos expuestos por la interfaz y no los elementos internos. Si bien puedo entender conceptualmente esa afirmación, a menudo encuentro que necesito probar métodos privados de forma aislada. Por ejemplo, cuando un método público llama a un método auxiliar privado que contiene alguna lógica no trivial.


¿Por qué la prueba de unidad necesita acceso a las partes internas de la implementación de A?

La prueba de la unidad debe estar probando A, y como tal solo debe preocuparse por la entrada y salida de A directamente. Si algo no es visible en la interfaz de A (ya sea directa o indirectamente), es posible que no tenga que ser parte de Aimpl (ya que sus resultados no son visibles para el mundo externo).

Si Aimpl genera los efectos secundarios que necesita probar, eso indica que debe mirar su diseño.


El lenguaje pImpl hace que las pruebas sean mucho más fáciles. Es lo suficientemente extraño como para ver un conjunto de respuestas sobre el tema "no probar la implementación" para motivar a responder mucho después del OP.

En C ++ habitual, no basado en pimpl tiene una clase con campos públicos y privados. Los campos públicos son fáciles de probar, los campos privados son un poco más tediosos. Sin embargo, la división entre lo público y lo privado es importante, ya que disminuye el ancho de la API y generalmente facilita los cambios posteriores.

Al usar este idioma una mejor opción está disponible. Puede tener exactamente la misma interfaz "pública" que con una sola clase, pero ahora solo hay un campo privado que contiene un puntero de algún tipo, por ejemplo

class my_things { public: my_things(); ~my_things(); void do_something_important(int); int also_this(); private: struct my_things_real; std::unique_ptr<my_things_real> state; };

Se espera que la clase my_things_real sea visible en el mismo archivo fuente que el destructor de la clase visible externamente, pero no en el encabezado. No es parte de la interfaz pública, por lo que todos los campos pueden ser públicos.

void my_things::do_something_important(int x) { state->doit(x); } // etc class my_things_real // I''d probably write ''struct'' { public: int value; void doit(int x) { value = x; } int getit() { return value; } };

Las pruebas unitarias se escriben contra la clase real. Prueba tanto o tan poco como quieras. Lo he llamado deliberadamente "real" en lugar de "implícito" para ayudar a garantizar que no se confunda con un mero detalle de implementación.

Probar esta clase es muy fácil ya que todos los campos son públicos. La interfaz externa es muy pequeña ya que está definida por la otra clase. Es difícil equivocarse en la capa de traducción wafer-thin, pero aún así puede probarla a través de la API externa. Esta es una clara victoria de la implementación y la interfaz que separa de manera más significativa.

En una nota vagamente relacionada, me parece absurdo que tantas personas coherentes aboguen por saltarse las pruebas de unidad para cualquier cosa que no sea fácilmente accesible a través de la API externa. Las funciones de nivel más bajo no son inmunes a los errores del programador. Las pruebas para verificar que la API sea utilizable son importantes y ortogonales para verificar que los detalles de la implementación sean correctos.


La idea detrás de pimpl es no tanto ocultar los detalles de implementación de las clases (los miembros privados ya lo hacen) sino mover los detalles de implementación fuera del encabezado. El problema es que en el modelo de inclusión de C ++, cambiar los métodos / variables privados obligará a que se vuelva a compilar cualquier archivo que incluya este archivo. Eso es un dolor, y es por eso que pimpl busca eliminar. No ayuda con la prevención de dependencias en bibliotecas externas. Otras técnicas hacen eso.

Sus pruebas unitarias no deberían depender de la implementación de la clase. Deben verificar que tu clase realmente actúa como debería. Lo único que realmente importa es cómo el objeto interactúa con el mundo exterior. Cualquier comportamiento que sus pruebas no puedan detectar debe ser interno al objeto y, por lo tanto, irrelevante.

Dicho esto, si encuentra demasiada complejidad dentro de la implementación interna de una clase, es posible que desee dividir esa lógica en un objeto o función independiente. Esencialmente, si su comportamiento interno es demasiado complejo para probar indirectamente, conviértalo en el comportamiento externo de otro objeto y pruebe eso.

Por ejemplo, supongamos que tengo una clase que toma una cadena como parámetro a su constructor. La cadena es real, un pequeño mini-lenguaje que especifica algo del comportamiento del objeto. (La cadena probablemente proviene de un archivo de configuración o algo así). En teoría, debería poder probar el análisis de esa cadena construyendo diferentes objetos y comprobando el comportamiento. Pero si el mini-lenguaje es lo suficientemente complejo, esto será difícil. Entonces, defino otra función que toma la cadena y devuelve una representación del contexto (como una matriz asociativa o algo así). Entonces puedo probar esa función de análisis por separado del objeto principal.


La prueba de unidad debe poner a la clase de implementación a su ritmo. Una vez que la clase PIMPL está en la imagen, ya estás en el ámbito de la "integración" y, por lo tanto, U / T no se aplica como tal. PIMPL se trata de ocultar la implementación; se supone que no debe conocer la configuración de clase de la implementación.


Si está realizando la inyección de dependencia correctamente, cualquier clase de dependencia A debería pasar a través de su interfaz pública: si su pImpl está interfiriendo con sus pruebas debido a las dependencias, parece que no está inyectando esas dependencias.

Las pruebas unitarias solo deben ocuparse de la interfaz pública que expone la clase A; Lo que A hace internamente con las dependencias no es su preocupación. Siempre que todo se inyecte correctamente, debería poder pasar por alto sin necesidad de preocuparse por la implementación interna de A. En cierto sentido, se podría decir que la capacidad de prueba y la pImpl correcta van de la mano, en el sentido de que una implementación no verificable oculta detalles que no deben ocultarse.