¿Qué sucede con las pruebas de métodos cuando ese método se vuelve privado después del rediseño en TDD?

29

Digamos que comienzo a desarrollar un juego de roles con personajes que atacan a otros personajes y ese tipo de cosas.

Aplicando TDD, hago algunos casos de prueba para probar la lógica dentro del Character.receiveAttack(Int)método. Algo como esto:

@Test
fun healthIsReducedWhenCharacterIsAttacked() {
    val c = Character(100) //arg is the health
    c.receiveAttack(50) //arg is the suffered attack damage
    assertThat(c.health, is(50));
}

Digamos que tengo 10 métodos de prueba receiveAttack. Ahora, agrego un método Character.attack(Character)(que llama receiveAttackmétodo), y después de algunos ciclos de TDD probándolo, tomo una decisión: Character.receiveAttack(Int)debería ser private.

¿Qué sucede con los 10 casos de prueba anteriores? ¿Debo eliminarlos? ¿Debo mantener el método public(no lo creo)?

Esta pregunta no trata sobre cómo probar métodos privados, sino cómo tratarlos después de un rediseño al aplicar TDD

Héctor
fuente
2
Posible duplicado de Prueba de métodos privados como protegido
mosquito
10
Si es privado, no lo prueba, es así de fácil.
Quítate
66
Probablemente voy contra la corriente aquí. Pero, generalmente evito los métodos privados a toda costa. Prefiero más pruebas que menos pruebas. Sé lo que la gente piensa "¿Qué, entonces nunca tienes ningún tipo de funcionalidad que no quieras exponer al consumidor?". Sí, tengo muchas cosas que no quiero exponer. En cambio, cuando tengo un método privado, lo refactorizo ​​en su propia clase y uso dicha clase de la clase original. La nueva clase se puede marcar como internalo el equivalente de su idioma para evitar que quede expuesta. De hecho, la respuesta de Kevin Cline es este tipo de enfoque.
user9993
3
@ user9993 parece que lo tienes al revés. Si es importante que le realicen más pruebas, la única manera de asegurarse de que no se haya perdido nada importante es realizar un análisis de cobertura. Y para las herramientas de cobertura no importa en absoluto si el método es privado o público o cualquier otra cosa. Con la esperanza de que se hace pública la materia de alguna manera compensar la falta de análisis de la cobertura da una falsa sensación de seguridad me temo
mosquito
2
@gnat ¿Pero nunca dije nada sobre "no tener cobertura"? Mi comentario sobre "Prefiero más pruebas que menos pruebas" debería haberlo hecho obvio. No estoy seguro de a qué te refieres exactamente, por supuesto, también pruebo el código que he extraído. Ese es todo el punto.
user9993

Respuestas:

52

En TDD, las pruebas sirven como documentación ejecutable de su diseño. Su diseño cambió, así que, obviamente, ¡su documentación también debe hacerlo!

Tenga en cuenta que, en TDD, la única forma en que attackpodría haber aparecido el método es como resultado de pasar una prueba fallida. Lo que significa que attackestá siendo probado por alguna otra prueba. Lo que significa que indirectamente receiveAttack está cubierto por attacklas pruebas de. Idealmente, cualquier cambio a receiveAttackdebería romper al menos una de attacklas pruebas.

Y si no es así, ¡hay una funcionalidad receiveAttackque ya no es necesaria y ya no debería existir!

Entonces, dado receiveAttackque ya se ha probado attack, no importa si mantiene o no sus pruebas. Si su marco de prueba facilita la prueba de métodos privados, y si decide probar métodos privados, puede conservarlos. Pero también puede eliminarlos sin perder la cobertura de prueba y la confianza.

Jörg W Mittag
fuente
14
Esta es una buena respuesta, guarde para "Si su marco de prueba facilita la prueba de métodos privados, y si decide probar métodos privados, puede conservarlos". Los métodos privados son detalles de implementación y nunca, nunca deben probarse directamente.
David Arno
20
@DavidArno: No estoy de acuerdo, por ese razonamiento nunca se debe probar lo interno de un módulo. Sin embargo, las partes internas de un módulo pueden ser de gran complejidad y, por lo tanto, tener pruebas unitarias para cada funcionalidad interna individual puede ser valioso. Las pruebas unitarias se utilizan para verificar los invariantes de una pieza de funcionalidad, si un método privado tiene invariantes (condiciones previas / condiciones posteriores), entonces una prueba unitaria puede ser valiosa.
Matthieu M.
8
" por ese razonamiento, los componentes internos de un módulo nunca deben ser probados ". Esas partes internas nunca deben ser probadas directamente . Todas las pruebas solo deben probar las API públicas. Si no se puede acceder a un elemento interno a través de una API pública, bórrelo ya que no hace nada.
David Arno
28
@DavidArno Según esa lógica, si está creando un ejecutable (en lugar de una biblioteca), entonces no debería tener pruebas unitarias en absoluto. - "¡Las llamadas a funciones no son parte de la API pública! ¡Solo lo son los argumentos de la línea de comandos! Si una función interna de su programa no es accesible a través de un argumento de la línea de comandos, elimínela ya que no hace nada". - Si bien las funciones privadas no son parte de la API pública de la clase, son parte de la API interna de la clase. Y aunque no necesariamente necesita probar la API interna de una clase, puede hacerlo, utilizando la misma lógica para probar la API interna de un ejecutable.
RM
77
@RM, si creara un ejecutable de una manera no modular, entonces me vería obligado a elegir entre pruebas frágiles de las partes internas, o solo usar pruebas de integración usando el E / S ejecutable y el tiempo de ejecución. Por lo tanto, según mi lógica real, en lugar de su versión de Strawman, lo crearía de forma modular (por ejemplo, a través de un conjunto de bibliotecas). Las API públicas de esos módulos se pueden probar de manera no frágil.
David Arno
23

Si el método es lo suficientemente complejo como para necesitar pruebas, debería ser público en alguna clase. Entonces refactoriza desde:

public class X {
  private int complexity(...) {
    ...
  }
  public void somethingElse() {
    int c = complexity(...);
  }
}

a:

public class Complexity {
  public int calculate(...) {
    ...
  }
}

public class X {
  private Complexity complexity;
  public X(Complexity complexity) { // dependency injection happiness
    this.complexity = complexity;
  }

  public void something() {
    int c = complexity.calculate(...);
  }
}

Mueva la prueba actual para X.complexity a ComplexityTest. Luego envía un mensaje de texto a X burlándose de Complejidad.

En mi experiencia, refactorizar hacia clases más pequeñas y métodos más cortos ofrece enormes beneficios. Son más fáciles de entender, más fáciles de probar y terminan siendo reutilizados más de lo que cabría esperar.

Kevin Cline
fuente
Su respuesta explica mucho más claramente la idea que estaba tratando de explicar en mi comentario sobre la pregunta de OP. Buena respuesta.
user9993
3
Gracias por tu respuesta. En realidad, el método ReceiveAttack es bastante simple ( this.health = this.health - attackDamage). Quizás extraerlo a otra clase es una solución de ingeniería excesiva, para este momento.
Héctor
1
Esto definitivamente es una exageración para el OP: quiere conducir a la tienda, no volar a la luna, pero es una buena solución en el caso general.
Si la función es así de simple, tal vez sea un exceso de ingeniería que incluso se define como una función en primer lugar.
David K
1
Puede ser excesivo hoy, pero dentro de 6 meses, cuando haya un montón de cambios en este código, los beneficios serán claros. Y en cualquier IDE decente en estos días, seguramente la extracción de algún código a una clase separada debería ser un par de pulsaciones de teclas, como mucho, una solución sobredimensionada, considerando que en el binario de tiempo de ejecución todo se reducirá a la misma de todos modos.
Stephen Byrne
6

Digamos que tengo 10 métodos de prueba para recibir el método Attack. Ahora, agrego un método Character.attack (Character) (que llama al método ReceiveAttack), y después de algunos ciclos TDD probándolo, tomo una decisión: Character.receiveAttack (Int) debería ser privado.

Algo a tener en cuenta aquí es que la decisión que está tomando es eliminar un método de la API . Las cortesías de la compatibilidad con versiones anteriores sugerirían

  1. Si no necesita eliminarlo, déjelo en la API
  2. Si no es necesario quitarlo sin embargo , a continuación, marcarlo como obsoleta y si es posible cuando el documento final de la vida va a suceder
  3. Si necesita eliminarlo, entonces tiene un cambio importante de versión

Las pruebas se eliminan o reemplazan cuando su API ya no admite el método. En ese momento, el método privado es un detalle de implementación que debería poder refactorizar.

En ese punto, regresa a la pregunta estándar de si su conjunto de pruebas debe acceder directamente a las implementaciones, en lugar de interactuar únicamente a través de la API pública. Un método privado es algo que deberíamos poder reemplazar sin que el conjunto de pruebas se interponga en el camino . Por lo tanto, esperaría que las pruebas se eliminen, ya sea que se retiren o se trasladen con la implementación a un componente que se pueda probar por separado.

VoiceOfUnreason
fuente
3
La desaprobación no siempre es una preocupación. De la pregunta: "Digamos que empiezo a desarrollar ..." si el software aún no se ha lanzado, la desaprobación no es un problema. Además: "un juego de roles" implica que no se trata de una biblioteca reutilizable, sino de un software binario dirigido a usuarios finales. Si bien algunos programas de usuario final tienen una API pública (por ejemplo, MS Office), la mayoría no. Incluso el software que lo hace tener una API pública tiene solamente una parte de ella se exponen para los plugins, secuencias de comandos (por ejemplo, juegos con extensión LUA), u otras funciones. Aún así, vale la pena plantear la idea para el caso general que describe el OP.