¿Está bien falsificar parte de la clase bajo prueba?

22

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 ()?

Usuario
fuente
Si prueba un código simultáneamente, ¿cómo puede ser malo? ¿Cuál es el problema? Normalmente, debería ser mejor probar el código con más frecuencia e intensidad.
Usuario desconocido
Acabo de enterarme de su actualización, he actualizado mi respuesta
Winston Ewert,

Respuestas:

27

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:

class NewYork
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}


class Miami
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}

class MyProfit
{
     MyProfit(NewYork new_york, Miami miami);
     boolean bothProfitable();
}

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:

Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");

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?

  1. Haga que las pruebas se ejecuten de manera consistente (evite acceder a cosas que cambian de una ejecución a otra)
  2. Evite acceder a recursos caros (no acceda a servicios de terceros, etc.)
  3. Simplifica el sistema bajo prueba
  4. Facilite la prueba de todos los escenarios posibles (es decir, cosas como simular fallas, etc.)
  5. Evite depender de los detalles de otras piezas de código para que los cambios en esas otras piezas de código no rompan esta prueba.

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:

class PersonBase
{
      abstract sring FirstName();
      abstract string LastName();

      string FullName()
      {
            return FirstName() + " " + LastName();
      }
 }

 class Person extends PersonBase
 {
      string FirstName(); 
      string LastName();
 }

 class FakePerson extends PersonBase
 {
      void setFirstName(string);
      void setLastName(string);
      string getFirstName();
      string getLastName();
 }

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.

Winston Ewert
fuente
8
Bien dicho. Si está tratando de encontrar soluciones alternativas en las pruebas, probablemente necesite reconsiderar su diseño (como nota al margen, ahora tengo la voz de Frank Sinatra atrapada en mi cabeza).
Problemática el
2
"Al burlarse de los métodos públicos para probar el objeto, está asumiendo [cómo] se implementa ese objeto". Pero, ¿no es ese el caso cada vez que golpea un objeto? Por ejemplo, supongamos que sé que mi método xyz () llama a algún otro objeto. Para probar xyz () de forma aislada, debo tropezar con el otro objeto. Por lo tanto, mi prueba debe conocer los detalles de implementación de mi método xyz ().
Usuario
En mi caso, los métodos "FirstName ()" y "LastName ()" son simples, pero consultan el resultado de una API de terceros.
Usuario
@Usuario, actualizado, probablemente se esté burlando de la API de terceros para probar FirstName y LastName. ¿Qué tiene de malo hacer la misma burla al probar FullName?
Winston Ewert
@ Winston, ese es mi punto principal, actualmente me burlo de la API de terceros utilizada en nombre y apellido para probar el nombre completo (), pero preferiría no preocuparme por cómo se implementan el nombre y el apellido al probar el nombre completo (por supuesto está bien cuando estoy probando nombre y apellido). De ahí mi pregunta sobre burlarse del nombre y apellido.
Usuario
10

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()y getRevenue(), luego pruebe los getProfit()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.

Problemático
fuente
1
Convenido. Pero solo para enfatizar el punto para cualquiera que lea esto, esta es la solución que usa si no puede hacer la mejor solución.
Winston Ewert
55
@ Winston, y esta es la solución que utiliza antes de llegar a una solución mejor. Es decir, si tiene una gran bola de código heredado, debe cubrirlo con pruebas unitarias antes de poder refactorizarlo.
Péter Török
@ Péter Török, buen punto. Aunque creo que está cubierto en "no se puede hacer la mejor solución". :)
Winston Ewert
@all Probar el método getProfit () será muy difícil ya que los diferentes escenarios para getExpenses () y getRevenues () en realidad multiplicarán los escenarios necesarios para getProfit (). si estamos probando getExpenses () y get Revenues () de forma independiente ¿No es una buena idea burlarse de estos métodos para probar getProfit ()?
Mohd Farid
7

Para simplificar aún más sus ejemplos, digamos que está probando C(), que depende de A()y B(), 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 de A()y, B()entonces, probablemente sea más fácil y mejor descartar A()y B(). Probablemente los puristas lo llamarían prueba unitaria.

Si está probando el comportamiento de todo el sistema (desde C()la perspectiva de), me iría A()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.

Rebecca Scott
fuente