Supongamos que tengo una clase (perdona el ejemplo artificial y el mal diseño de la misma):
class MyProfit
{
public decimal GetNewYorkRevenue();
public decimal GetNewYorkExpenses();
public decimal GetNewYorkProfit();
public decimal GetMiamiRevenue();
public decimal GetMiamiExpenses();
public decimal GetMiamiProfit();
public bool BothCitiesProfitable();
}
(Tenga en cuenta que los métodos GetxxxRevenue () y GetxxxExpenses () tienen dependencias que están desactivadas)
Ahora estoy probando la unidad BothCitiesProfitable () que depende de GetNewYorkProfit () y GetMiamiProfit (). ¿Está bien apurar GetNewYorkProfit () y GetMiamiProfit ()?
Parece que si no lo hago, estoy probando simultáneamente GetNewYorkProfit () y GetMiamiProfit () junto con BothCitiesProfitable (). Tendría que asegurarme de configurar el apéndice para GetxxxRevenue () y GetxxxExpenses () para que los métodos GetxxxProfit () devuelvan los valores correctos.
Hasta ahora, solo he visto ejemplos de dependencias de stubbing en clases externas, no en métodos internos.
Y si está bien, ¿hay algún patrón en particular que deba usar para hacer esto?
ACTUALIZAR
Me preocupa que estemos perdiendo el problema central y que probablemente sea culpa de mi pobre ejemplo. La pregunta fundamental es: si un método en una clase depende de otro método expuesto públicamente en esa misma clase, ¿está bien (o incluso se recomienda) eliminar ese otro método?
Tal vez me estoy perdiendo algo, pero no estoy seguro de que dividir la clase siempre tenga sentido. Quizás otro ejemplo mínimamente mejor sería:
class Person
{
public string FirstName()
public string LastName()
public string FullName()
}
donde el nombre completo se define como:
public string FullName()
{
return FirstName() + " " + LastName();
}
¿Está bien apilar el nombre () y el apellido () al probar el nombre completo ()?
fuente
Respuestas:
Deberías dividir la clase en cuestión.
Cada clase debe hacer una tarea simple. Si su tarea es demasiado complicada para probar, entonces la tarea que realiza la clase es demasiado grande.
Ignorando la tontería de este diseño:
ACTUALIZAR
El problema con los métodos de copia en una clase es que está violando la encapsulación. Su prueba debe estar verificando si el comportamiento externo del objeto coincide o no con las especificaciones. Lo que ocurra dentro del objeto no es asunto suyo.
El hecho de que FullName use FirstName y LastName es un detalle de implementación. Nada fuera de la clase debería importarle que eso sea cierto. Al burlarse de los métodos públicos para probar el objeto, está asumiendo que se implementa ese objeto.
En algún momento en el futuro, esa suposición puede dejar de ser correcta. Quizás toda la lógica del nombre se reubicará en un objeto Nombre que la persona simplemente llama. Quizás FullName accederá directamente a las variables miembro first_name y last_name en lugar de llamar a FirstName y LastName.
La segunda pregunta es por qué siente la necesidad de hacerlo. Después de todo, tu clase de persona podría probarse algo como:
No deberías sentir la necesidad de nada para este ejemplo. Si lo haces, entonces estás feliz y bien ... ¡para! No hay ningún beneficio en burlarse de los métodos allí porque de todos modos tienes control sobre lo que hay en ellos.
El único caso en el que parece tener sentido para los métodos que FullName utiliza para burlarse es si de alguna manera FirstName () y LastName () eran operaciones no triviales. Tal vez está escribiendo uno de esos generadores de nombres aleatorios, o FirstName y LastName consultan la base de datos para obtener una respuesta, o algo así. Pero si eso es lo que sucede, sugiere que el objeto está haciendo algo que no pertenece a la clase Persona.
Dicho de otra manera, burlándose de los métodos es tomar el objeto y partirlo en dos pedazos. Se burlan de una pieza mientras se prueba la otra pieza. Lo que está haciendo es esencialmente una ruptura ad-hoc del objeto. Si ese es el caso, simplemente rompa el objeto ya.
Si su clase es simple, no debe sentir la necesidad de burlarse de ella durante una prueba. Si su clase es lo suficientemente compleja como para sentir la necesidad de burlarse, entonces debe dividir la clase en partes más simples.
ACTUALIZAR DE NUEVO
A mi modo de ver, un objeto tiene un comportamiento externo e interno. El comportamiento externo incluye llamadas de valores devueltos a otros objetos, etc. Obviamente, cualquier cosa en esa categoría debe ser probada. (de lo contrario, ¿qué probaría?) Pero el comportamiento interno no debería ser realmente probado.
Ahora se prueba el comportamiento interno, porque es lo que resulta en el comportamiento externo. Pero no escribo pruebas directamente sobre el comportamiento interno, solo indirectamente a través del comportamiento externo.
Si quiero probar algo, creo que debe moverse para que se convierta en un comportamiento externo. Es por eso que creo que si quieres burlarte de algo, debes dividir el objeto para que lo que quieras burlarte esté ahora en el comportamiento externo de los objetos en cuestión.
Pero, ¿qué diferencia hace? Si FirstName () y LastName () son miembros de otro objeto, ¿realmente cambia el problema de FullName ()? Si decidimos que es necesario burlarse de FirstName y LastName, ¿es realmente útil para ellos estar en otro objeto?
Creo que si usas tu enfoque burlón, entonces creas una costura en el objeto. Tiene funciones como FirstName () y LastName () que se comunican directamente con una fuente de datos externa. También tiene FullName () que no tiene. Pero dado que todos están en la misma clase, eso no es evidente. Se supone que algunas piezas no deben acceder directamente a la fuente de datos y otras sí. Su código será más claro si solo separa esos dos grupos.
EDITAR
Retrocedamos un paso y preguntemos: ¿por qué nos burlamos de los objetos cuando probamos?
Ahora, creo que las razones 1-4 no se aplican a este escenario. Burlarse de la fuente externa cuando se prueba el nombre completo se ocupa de todas esas razones para burlarse. La única pieza que no se maneja es la simplicidad, pero parece que el objeto es lo suficientemente simple, eso no es una preocupación.
Creo que su preocupación es la razón número 5. La preocupación es que en algún momento en el futuro cambiar la implementación de FirstName y LastName interrumpirá la prueba. En el futuro, FirstName y LastName pueden obtener los nombres de una ubicación o fuente diferente. Pero FullName probablemente siempre lo será
FirstName() + " " + LastName()
. Es por eso que desea probar FullName burlándose de FirstName y LastName.Lo que tienes entonces es un subconjunto del objeto persona que es más probable que cambie que los demás. El resto del objeto usa este subconjunto. Ese subconjunto actualmente obtiene sus datos utilizando una fuente, pero puede obtener esos datos de una manera completamente diferente en una fecha posterior. Pero para mí parece que ese subconjunto es un objeto distinto que intenta salir.
Me parece que si te burlas del método del objeto, lo estás dividiendo. Pero lo está haciendo de manera ad-hoc. Su código no deja en claro que hay dos piezas distintas dentro de su objeto Persona. Entonces, simplemente divida ese objeto en su código real, para que quede claro al leer su código lo que está sucediendo. Elija la división real del objeto que tenga sentido y no intente dividir el objeto de manera diferente para cada prueba.
Sospecho que puede objetar dividir su objeto, pero ¿por qué?
EDITAR
Estaba equivocado.
Debería dividir los objetos en lugar de introducir divisiones ad-hoc burlándose de métodos individuales. Sin embargo, estaba demasiado concentrado en el único método para dividir objetos. Sin embargo, OO proporciona múltiples métodos para dividir un objeto.
Lo que propondría:
Tal vez eso es lo que estabas haciendo todo el tiempo. Pero no creo que este método tenga los problemas que vi con los métodos de burla porque hemos delineado claramente de qué lado está cada método. Y al usar la herencia, evitamos la incomodidad que surgiría si usáramos un objeto contenedor adicional.
Esto introduce cierta complejidad, y solo para un par de funciones de utilidad probablemente las probaría burlándome de la fuente de terceros subyacente. Claro, corren un mayor riesgo de romperse, pero no vale la pena reorganizarlos. Si tienes un objeto lo suficientemente complejo que necesitas dividirlo, entonces creo que algo como esto es una buena idea.
fuente
Si bien estoy de acuerdo con la respuesta de Winston Ewert, a veces simplemente no es posible dividir una clase, ya sea debido a limitaciones de tiempo, la API ya se está utilizando en otro lugar, o lo que sea.
En un caso como ese, en lugar de burlarse de los métodos, escribiría mis pruebas para cubrir la clase de forma incremental. Pruebe los métodos
getExpenses()
ygetRevenue()
, luego pruebe losgetProfit()
métodos, luego pruebe los métodos compartidos. Es cierto que tendrá más de una prueba que cubre un método en particular, pero dado que escribió pruebas de aprobación para cubrir los métodos individualmente, puede estar seguro de que sus resultados son confiables dada una entrada probada.fuente
Para simplificar aún más sus ejemplos, digamos que está probando
C()
, que depende deA()
yB()
, cada uno de los cuales tiene sus propias dependencias. En mi opinión, todo se reduce a lo que tu prueba está tratando de lograr.Si está probando el comportamiento de
C()
determinados comportamientos conocidos deA()
y,B()
entonces, probablemente sea más fácil y mejor descartarA()
yB()
. Probablemente los puristas lo llamarían prueba unitaria.Si está probando el comportamiento de todo el sistema (desde
C()
la perspectiva de), me iríaA()
y,B()
tal como está implementado, eliminaría sus dependencias (si es posible hacer pruebas) o configuraría un entorno de sandbox, como una prueba base de datos. Yo llamaría a esto una prueba de integración.Ambos enfoques pueden ser válidos, por lo que si está diseñado para que pueda probarlo de cualquier manera, probablemente será mejor a largo plazo.
fuente