Método privado de prueba unitaria en c ++ usando una clase de amigo

15

Sé que esta es una práctica debatida, pero supongamos que esta es la mejor opción para mí. Me pregunto cuál es la técnica real para hacer esto. El enfoque que veo es este:

1) Haz que una clase de amigo sea la de la clase cuyo método quiero probar.

2) En la clase amigo, cree uno o varios métodos públicos que llamen a los métodos privados de la clase probada.

3) Prueba los métodos públicos de la clase de amigos.

Aquí hay un ejemplo simple para ilustrar los pasos anteriores:

#include <iostream>

class MyClass
{
  friend class MyFriend; // Step 1

  private:
  int plus_two(int a)
  {
    return a + 2;
  }
};

class MyFriend
{
public:
  MyFriend(MyClass *mc_ptr_1)
  {
    MyClass *mc_ptr = mc_ptr_1;
  }

  int plus_two(int a) // Step 2
  {
    return mc_ptr->plus_two(a);
  }
private:
  MyClass *mc_ptr;
};

int main()
{
  MyClass mc;
  MyFriend mf(&mc);
  if (mf.plus_two(3) == 5) // Step 3
    {
      std::cout << "Passed" << std::endl;
    }
  else
    {
      std::cout << "Failed " << std::endl;
    }

  return 0;
}

Editar:

Veo que en la discusión que sigue a una de las respuestas la gente se pregunta sobre mi código base.

Mi clase tiene métodos que son llamados por otros métodos; ninguno de estos métodos debería llamarse fuera de la clase, por lo que deberían ser privados. Por supuesto, se podrían poner en un método, pero lógicamente están mucho mejor separados. Estos métodos son lo suficientemente complicados como para justificar las pruebas unitarias y, debido a problemas de rendimiento, es muy probable que tenga que volver a factorizar estos métodos, por lo tanto, sería bueno tener una prueba para asegurarme de que mi refactorización no rompa nada. No soy el único que trabaja en el equipo, aunque soy el único que está trabajando en este proyecto, incluidas las pruebas.

Habiendo dicho lo anterior, mi pregunta no era sobre si es una buena práctica escribir pruebas unitarias para métodos privados, aunque agradezco los comentarios.

Akavall
fuente
55
Aquí hay una sugerencia cuestionable para una pregunta cuestionable. No me gusta el acoplamiento de un amigo ya que el código lanzado tiene que saber sobre la prueba. La respuesta de Nir a continuación es una forma de aliviar eso, pero todavía no me gusta mucho cambiar la clase para que se ajuste a la prueba. Como no confío en la herencia a menudo, a veces solo protejo los métodos privados y hago que una clase de prueba herede y exponga según sea necesario. Espero al menos tres "abucheos" para este comentario, pero la realidad es que la API pública y la API de prueba pueden diferir y seguir siendo distintas de la API privada. Meh
J Trana
44
@JTrana: ¿Por qué no escribir eso como una respuesta adecuada?
Bart van Ingen Schenau
Okay. Este no es uno de esos de los que te sientes muy orgulloso, pero espero que te ayude.
J Trana

Respuestas:

23

Una alternativa a amigo (bueno, en cierto sentido) que uso con frecuencia es un patrón que he llegado a conocer como access_by. Es bastante simple:

class A {
  void priv_method(){};
 public:
  template <class T> struct access_by;
  template <class T> friend struct access_by;
}

Ahora, suponga que la clase B participa en la prueba A. Puede escribir esto:

template <> struct access_by<B> {
  call_priv_method(A & a) {a.priv_method();}
}

Luego puede usar esta especialización de access_by para llamar a métodos privados de A. Básicamente, lo que hace es poner la responsabilidad de declarar amistad en el archivo de encabezado de la clase que quiere llamar a los métodos privados de A. También le permite agregar amigos a A sin cambiar la fuente de A. Idiomáticamente, también indica a quien lea la fuente de A que A no le indica a B un verdadero amigo en el sentido de extender su interfaz. Por el contrario, la interfaz de A está completa como se indica y B necesita acceso especial a A (las pruebas son un buen ejemplo, también he usado este patrón al implementar enlaces de python boost, a veces una función que debe ser privada en C ++ es útil para exponer en la capa de python para la implementación).

Nir Friedman
fuente
friend access_by¿Tiene curiosidad acerca de un caso de uso válido para hacer que el , no sea el primer no amigo suficiente: al ser una estructura anidada, tendría acceso a todo dentro de A? p.ej. coliru.stacked-crooked.com/a/663dd17ed2acd7a3
fuerte el
10

Si es difícil de probar, está mal escrito

Si tiene una clase con métodos privados lo suficientemente complejos como para justificar su propia prueba, la clase está haciendo demasiado. Dentro hay otra clase tratando de salir.

Extraiga los métodos privados que desea probar en una nueva clase (o clases) y haga que sean públicos. Prueba las nuevas clases.

Además de hacer que el código sea más fácil de probar, esta refactorización hará que el código sea más fácil de entender y mantener.

Kevin Cline
fuente
1
Estoy completamente de acuerdo con esta respuesta, si no puede probar sus métodos privados por completo probando los métodos públicos, algo no está bien y refactorizar los métodos privados a su propia clase sería una buena solución.
David realiza el
44
En mi base de código, una clase tiene un método muy complejo que inicializa un gráfico computacional. Llama a varias subfunciones en secuencia para lograr varios aspectos de esto. Cada subfunción es bastante complicada, y la suma total del código es muy complicada. Sin embargo, las subfunciones no tienen sentido si no se invocan en esta clase y en la secuencia correcta. Lo único que le importa al usuario es que el gráfico computacional se inicialice completamente; los intermedios no valen nada para el usuario. Por favor, me gustaría saber cómo debería refactorizar esto y por qué tiene más sentido que simplemente probar los métodos privados.
Nir Friedman
1
@Nir: Haz lo trivial: extrae una clase con todos esos métodos públicos y convierte tu clase existente en una fachada alrededor de la nueva clase.
Kevin Cline
Esta respuesta es correcta, pero realmente depende de los datos con los que está trabajando. En mi caso, no se me proporcionan datos de prueba reales, así que tengo que crearlos observando datos en tiempo real y luego "inyectarlos" en mi aplicación y ver cómo Una sola pieza de datos es demasiado compleja de manejar, por lo que sería mucho más fácil crear artificialmente datos de prueba parciales que son solo un subconjunto de los datos en tiempo real para apuntar a cada uno que reproducir los datos en tiempo real. La función privada no es compleja suficiente para solicitar la implementación de varias otras clases pequeñas (con la funcionalidad respectiva)
rbaleksandar
4

No deberías probar métodos privados. Período. Las clases que usan su clase solo se preocupan por los métodos que proporciona, no por los que usa debajo del capó para trabajar.

Si le preocupa la cobertura de su código, necesita encontrar configuraciones que le permitan probar ese método privado desde una de las llamadas a métodos públicos. Si no puede hacer eso, ¿cuál es el punto de tener el método en primer lugar? Es simplemente un código inalcanzable.

Ampt
fuente
66
El objetivo de los métodos privados es facilitar el desarrollo (a través de la separación de las preocupaciones, o el mantenimiento de DRY, o cualquier cantidad de cosas), pero están destinados a ser no permanentes. Son privados por esa razón. Podrían aparecer, desaparecer o cambiar drásticamente la funcionalidad de una implementación a la siguiente, por lo que vincularlos a pruebas unitarias no siempre es práctico o incluso útil.
Ampt
8
Puede que no siempre sea práctico o útil, pero eso está muy lejos de decir que nunca deberías probarlos. Estás hablando de los métodos privados como si fueran los métodos privados de otra persona; "Podrían aparecer, desaparecer ...". No, no pudieron. Si la unidad los prueba directamente, solo debe ser porque los mantiene usted mismo. Si cambia la implementación, cambia las pruebas. En resumen, su declaración general no está justificada. Si bien es bueno advertir al OP sobre esto, su pregunta aún está justificada, y su respuesta en realidad no la responde.
Nir Friedman
2
Permítanme también señalar: el OP dijo que es consciente de que es una práctica debatida. Entonces, si él quiere hacerlo de todos modos, ¿tal vez realmente tiene buenas razones para ello? Ninguno de nosotros conoce los detalles de su base de código. En el código con el que trabajo, tenemos algunos programadores muy experimentados y expertos, y pensaron que era útil probar métodos privados en algunos casos.
Nir Friedman
2
-1, una respuesta como "No deberías probar métodos privados". en mi humilde opinión no es útil. El tema cuándo probar y cuándo no probar métodos privados se ha discutido en este sitio lo suficiente. El OP ha demostrado que está al tanto de esta discusión, y claramente está buscando soluciones bajo el supuesto de que en su caso probar métodos privados es el camino a seguir.
Doc Brown
3
Creo que el problema aquí es que este puede ser un problema XY muy clásico, ya que OP cree que necesita probar sus métodos privados por una razón u otra, cuando en realidad podría abordar las pruebas desde un punto de vista más pragmático, ver lo privado métodos como simples funciones de ayuda para los públicos, que son el contrato con los usuarios finales de la clase.
Ampt
3

Hay algunas opciones para hacer esto, pero tenga en cuenta que (en esencia) modifican la interfaz pública de sus módulos, para darle acceso a los detalles de implementación internos (transformando efectivamente las pruebas unitarias en dependencias de cliente estrechamente acopladas, donde debería tener sin dependencias en absoluto).

  • puede agregar una declaración de amigo (clase o función) a la clase probada.

  • puede agregar #define private publical comienzo de sus archivos de prueba, antes de #includemarcar el código probado. Sin embargo, en caso de que el código probado sea una biblioteca ya compilada, esto podría hacer que los encabezados ya no coincidan con el código binario ya compilado (y causar UB).

  • podría insertar una macro en su clase probada y decidir más adelante qué significa esa macro (con una definición diferente para probar el código). Esto le permitiría probar elementos internos, pero también permitiría que el código de un cliente de terceros piratee su clase (creando su propia definición en la declaración que agregue).

utnapistim
fuente
2

Aquí hay una sugerencia cuestionable para una pregunta cuestionable. No me gusta el acoplamiento de un amigo ya que el código lanzado tiene que saber sobre la prueba. La respuesta de Nir es una forma de aliviar eso, pero todavía no me gusta mucho cambiar la clase para que se ajuste a la prueba.

Como no confío en la herencia a menudo, a veces solo protejo los métodos privados y hago que una clase de prueba herede y exponga según sea necesario. La realidad es que la API pública y la API de prueba pueden diferir y seguir siendo distintas de la API privada, lo que lo deja en una especie de vínculo.

Aquí hay un ejemplo práctico para el que recurro a este truco. Escribo código incrustado y confiamos bastante en las máquinas de estado. La API externa no necesariamente necesita saber sobre el estado interno de la máquina de estado, pero la prueba debería (posiblemente) probar la conformidad con el diagrama de la máquina de estado en el documento de diseño. Podría exponer un getter de "estado actual" como protegido y luego dar acceso a la prueba a eso, lo que me permite probar la máquina de estado más completamente. A menudo encuentro este tipo de clase difícil de probar como una caja negra.

J Trana
fuente
Si bien es un enfoque de Java, esto es bastante estándar para que las funciones privadas tengan un nivel predeterminado en lugar de permitir que otras clases en el mismo paquete (las clases de prueba) puedan verlas.
0

Puede escribir su código con muchas soluciones alternativas para evitar que tenga que usar amigos.

Puedes escribir clases y nunca tener ningún método privado. Todo lo que necesita hacer es realizar funciones de implementación dentro de la unidad de compilación, dejar que su clase los llame y pasar cualquier miembro de datos al que necesiten acceder.

Y sí, significará que puede cambiar las firmas o agregar nuevos métodos de "implementación" sin cambiar su encabezado en el futuro.

Sin embargo, debe sopesar si vale la pena o no. Y mucho dependerá realmente de quién va a ver tu encabezado.

Si estoy usando una biblioteca de terceros, preferiría no ver declaraciones de amigos a sus evaluadores de unidades. Tampoco quiero construir su biblioteca y ejecutar sus pruebas cuando lo haga. Desafortunadamente, muchas bibliotecas de código abierto de terceros que he construido hacen eso.

La prueba es el trabajo de los escritores de la biblioteca, no de sus usuarios.

Sin embargo, no todas las clases son visibles para el usuario de su biblioteca. Muchas clases son "implementación" y usted las implementa de la mejor manera para garantizar que funcionen correctamente. En esos, aún puede tener miembros y métodos privados, pero desea que los probadores de unidades los prueben. Así que adelante y hágalo de esa manera si eso conducirá a un código robusto más rápido, eso es fácil de mantener para aquellos que necesitan hacerlo.

Si los usuarios de su clase están todos dentro de su propia empresa o equipo, también puede relajarse un poco más sobre esa estrategia, suponiendo que esté permitida por los estándares de codificación de su empresa.

CashCow
fuente