Desde la perspectiva de TDD, ¿soy una mala persona si comparo con un punto final en vivo en lugar de un simulacro?

16

Sigo TDD religiosamente. Mis proyectos suelen tener una cobertura de prueba del 85% o mejor, con casos de prueba significativos.

Trabajo mucho con HBase , y la interfaz principal del cliente, HTable, es muy difícil de burlar. Me toma 3 o 4 veces más tiempo escribir mis pruebas unitarias que escribir pruebas que usan un punto final en vivo.

Sé que, filosóficamente, las pruebas que usan simulacros deben tener prioridad sobre las pruebas que usan un punto final en vivo. Pero burlarse de HTable es un dolor grave, y no estoy realmente seguro de que ofrezca una gran ventaja sobre las pruebas contra una instancia de HBase en vivo.

Todos en mi equipo ejecutan una instancia de HBase de un solo nodo en su estación de trabajo, y tenemos instancias de HBase de un solo nodo ejecutándose en nuestros cuadros de Jenkins, por lo que no es un problema de disponibilidad. Las pruebas de punto final en vivo obviamente tardan más en ejecutarse que las pruebas que usan simulacros, pero realmente no nos importa.

En este momento, escribo pruebas de punto final en vivo y pruebas simuladas para todas mis clases. Me encantaría deshacerme de los simulacros, pero no quiero que la calidad disminuya como resultado.

¿Qué piensan todos ustedes?

sangre fría
fuente
8
El punto final en vivo no es realmente una prueba unitaria, ¿verdad? Es una prueba de integración. Pero finalmente es probablemente una cuestión de pragmatismo; puede pasar el tiempo escribiendo simulacros, o pasar el tiempo escribiendo características o arreglando errores.
Robert Harvey
44
He escuchado historias de personas que eliminan servicios de terceros ejecutando una prueba de unidad contra su propio código ... que se conectó a un punto final en vivo. La limitación de velocidad no es algo que las pruebas unitarias normalmente hagan o les importe.
14
No eres una mala persona. Eres una buena persona haciendo algo malo.
Kyralessa
15
Sigo TDD religiosamente ¿ Tal vez ese es el problema? No creo que ninguna de esta metodología está destinado a ser llevado que en serio. ;)
FrustratedWithFormsDesigner
9
Seguir TDD religiosamente significaría que descartas el código descubierto del 15%.
mouviciel

Respuestas:

23
  • Mi primera recomendación sería no burlarse de tipos que no son de su propiedad . Usted mencionó que HTable es una verdadera molestia: tal vez debería envolverlo en un Adaptador que expone el 20% de las características de HTable que necesita y burlarse del envoltorio donde sea necesario.

  • Dicho esto, supongamos que estamos hablando de tipos que todos poseen. Si sus pruebas basadas en simulacros se centran en escenarios de ruta feliz donde todo transcurre sin problemas, no perderá nada abandonándolos porque sus pruebas de integración probablemente ya estén probando exactamente las mismas rutas.

    Sin embargo, las pruebas aisladas se vuelven interesantes cuando comienzas a pensar cómo debería reaccionar tu sistema bajo prueba a cada pequeña cosa que podría suceder como se define en el contrato de su colaborador, independientemente del objeto concreto real con el que esté hablando. Eso es parte de lo que algunos llaman corrección básica . Podría haber muchos de esos pequeños casos y muchas más combinaciones de ellos. Aquí es donde las pruebas de integración comienzan a ser pésimas, mientras que las pruebas aisladas seguirán siendo rápidas y manejables.

    Para ser más concreto, ¿qué sucede si uno de los métodos de su adaptador HTable devuelve una lista vacía? ¿Qué pasa si devuelve nulo? ¿Qué pasa si arroja una excepción de conexión? Debe definirse en el contrato del Adaptador si cualquiera de esas cosas puede suceder, y cualquiera de sus consumidores debe estar preparado para lidiar con estas situaciones , de ahí la necesidad de pruebas para ellos.

En resumen: no verá ninguna disminución de la calidad al eliminar sus pruebas basadas en simulacros si probaron exactamente las mismas cosas que sus pruebas de integración . Sin embargo, tratar de imaginar pruebas aisladas adicionales (y pruebas de contrato ) puede ayudarlo a pensar ampliamente en sus interfaces / contratos y aumentar la calidad al abordar defectos que habrían sido difíciles de pensar y / o lentos para probar con las pruebas de integración.

guillaume31
fuente
+1 Me resulta mucho más fácil construir casos extremos con pruebas simuladas que llenar la base de datos con esos casos.
Rob
Estoy de acuerdo con la mayoría de tu respuesta. Sin embargo, no estoy seguro de estar de acuerdo con la parte del adaptador. Es difícil burlarse de HTable porque es bastante simple. Por ejemplo, si desea realizar una operación de obtención por lotes, debe crear un montón de objetos Get, colocarlos en una lista y luego llamar a HTable.batch (). Desde una perspectiva burlona, ​​esto es un dolor grave, porque debe crear un Matcher personalizado que examine la lista de objetos get que pasa a HTable.batch () y luego devuelve los resultados correctos para esa lista de objetos get (). Un dolor GRAVE.
sangfroid
Supongo que podría crear una clase de envoltura agradable y amigable para HTable que se encargara de toda esa limpieza, pero en ese momento ... siento que estoy construyendo un marco alrededor de HTable, ¿y ese debería ser realmente mi trabajo? Usualmente "¡Construyamos un marco!" es una señal de que voy en la dirección equivocada. Podría pasar días escribiendo clases para hacer que HBase sea más amigable, y no sé si es un gran uso de mi tiempo. Además, estoy analizando una interfaz o un contenedor en lugar de solo el viejo objeto HTable, y eso definitivamente haría que mi código sea más complejo.
sangfroid
Pero estoy de acuerdo con su punto principal, que uno no debe escribir simulacros para clases que no son de su propiedad. Y definitivamente estoy de acuerdo en que no tiene sentido escribir una prueba simulada que pruebe lo mismo que una prueba de integración. Parece que los simulacros son los mejores para probar interfaces / contratos. Gracias por el consejo, ¡esto me ha ayudado mucho!
sangfroid
Tengo poca idea de lo que HTable realmente hace y cómo lo usa, así que no tome mi ejemplo de envoltorio al pie de la letra. Había mencionado un envoltorio / adaptador porque pensé que el envoltorio era relativamente pequeño. No tiene que introducir una réplica individual a HTable, lo que, por supuesto, sería un fastidio, y mucho menos un marco completo, pero necesita una costura , una interfaz entre el ámbito de su aplicación y el de HTable. Debería reformular algunas de las funciones de HTable en los propios términos de su aplicación. El patrón de repositorio es una encarnación perfecta de tal costura cuando se trata de acceso a datos.
guillaume31
11

filosóficamente, las pruebas que usan simulacros deben tener prioridad sobre las pruebas que usan un punto final en vivo

Creo que por lo menos, eso es un punto de corriente continua controversia entre los defensores de TDD.

Mi opinión personal va más allá de eso para decir que una prueba simulada es principalmente una forma de representar una forma de contrato de interfaz ; idealmente se rompe (es decir, falla) si y solo si cambia la interfaz . Y como tal, en lenguajes razonablemente fuertemente tipados como Java, y cuando se usa una interfaz definida explícitamente, es casi completamente superfluo: el compilador ya le habrá dicho si ha cambiado la interfaz.

La principal excepción es cuando está utilizando una interfaz muy genérica, quizás basada en anotaciones o reflexiones, que el compilador no puede supervisar automáticamente de manera útil. Incluso entonces, debe verificar si hay una manera de hacer la validación mediante programación (por ejemplo, una biblioteca de verificación de sintaxis SQL) en lugar de hacerlo manualmente con simulacros.

Es el último caso que está haciendo cuando prueba con una base de datos local 'en vivo'; la implementación de htable se inicia y aplica una validación mucho más completa del contrato de interfaz de lo que se podría pensar escribir a mano.

Desafortunadamente, un uso mucho más común de las pruebas simuladas es la prueba que:

  • pasa por lo que sea el código en el momento en que se escribió la prueba
  • no proporciona garantías sobre ninguna propiedad del código que no sea la que existe y el tipo de ejecución
  • falla cada vez que cambia ese código

Tales pruebas, por supuesto, deben eliminarse a la vista.

soru
fuente
1
No puedo respaldar esto lo suficiente. Prefiero tener una cobertura de 1 pieza con excelentes pruebas que una cobertura de relleno de 100 piezas.
Ian
3
Las pruebas basadas en simulacros describen el contrato utilizado por 2 objetos para hablar juntos, pero van mucho más allá de lo que puede hacer el sistema de tipos de un lenguaje como Java. No se trata solo de firmas de métodos, también pueden especificar rangos de valores válidos para argumentos o resultados devueltos, qué excepciones son aceptables, en qué orden y cuántas veces se pueden llamar los métodos, etc. El compilador solo no le avisará si hay son cambios en esos. En ese sentido, no creo que sean superfluos en absoluto. Consulte infoq.com/presentations/integration-tests-scam para obtener más información sobre las pruebas basadas en simulacros.
guillaume31
1
... de acuerdo, es decir, probar la lógica en torno a la llamada de la interfaz
Rob
1
Ciertamente, puede agregar excepciones no verificadas, condiciones previas no declaradas y estado implícito a la lista de cosas que hacen que una interfaz sea menos estáticamente tipada, y así justificar las pruebas basadas en simulacros en lugar de una simple compilación. El problema, sin embargo, es que cuando estos aspectos hacen el cambio, su especificación es implícito y distribuido entre las pruebas de todos los clientes. Es probable que no se actualicen, por lo tanto, siéntese en silencio escondiendo un error detrás de una marca verde.
soru
"su especificación es implícita": no si escribe pruebas de contrato para sus interfaces ( blog.thecodewhisperer.com/2011/07/07/contract-tests-an-example ) y se adhiere a ellas al configurar simulacros.
guillaume31
5

¿Cuánto tiempo tarda más en ejecutarse una prueba basada en puntos finales que una prueba simulada? Si es significativamente más largo, entonces sí, vale la pena invertir su tiempo de redacción de pruebas para hacer que las pruebas unitarias sean más rápidas, porque tendrá que ejecutarlas muchas, muchas veces. Si no es significativamente más largo, a pesar de que las pruebas basadas en puntos finales no son pruebas unitarias "puras", siempre y cuando estén haciendo un buen trabajo al probar la unidad, no hay razón para ser religiosos al respecto.

Carl Manaster
fuente
4

Estoy completamente de acuerdo con la respuesta de guillaume31, ¡nunca te burles de los tipos que no tienes!

Normalmente, un problema en la prueba (burlándose de una interfaz compleja) refleja un problema en su diseño. Quizás necesite algo de abstracción entre su modelo y su código de acceso a datos, por ejemplo, utilizando una arquitectura hexagonal y un patrón de repositorio, es la forma más habitual de resolver este tipo de problemas.

Si desea hacer una prueba de integración para verificar las cosas, haga una prueba de integración, si desea hacer una prueba unitaria porque está probando su lógica, haga una prueba unitaria y aísle la persistencia. Pero hacer una prueba de integración porque no sabe cómo aislar su lógica de un sistema externo (o porque aislar es un dolor) es un gran olor, está eligiendo la integración sobre la unidad por una limitación en su diseño, no por una necesidad real para probar la integración.

Eche un vistazo a esta charla de Ian Cooper: http://vimeo.com/68375232 , habla sobre la arquitectura hexagonal y las pruebas, habla sobre cuándo y qué burlarse, una charla realmente inspirada que resuelve muchas preguntas como la suya sobre la TDD real .

AlfredoCasado
fuente
1

TL; DR : a mi modo de ver, depende de cuánto esfuerzo termine gastando en pruebas y de si hubiera sido mejor gastar más en su sistema real.

Versión larga:

Algunas buenas respuestas aquí, pero mi opinión al respecto es diferente: las pruebas son una actividad económica que debe pagarse por sí misma, y ​​si el tiempo que pasa no se devuelve en el desarrollo y la confiabilidad del sistema (o cualquier otra cosa que desee obtener) de pruebas), entonces puede estar haciendo una mala inversión; estás en el negocio de construir sistemas, no de escribir pruebas. Por lo tanto, reducir el esfuerzo para escribir y mantener pruebas es crucial.

Por ejemplo, algunos valores principales que obtengo de las pruebas son:

  • Fiabilidad (y, por lo tanto, velocidad de desarrollo): refactorice el código / integre un nuevo marco / cambie un componente / puerto a una plataforma diferente, asegúrese de que todo funcione
  • Comentarios de diseño: comentarios clásicos de TDD / BDD "use su código" en sus interfaces de nivel bajo / medio

La prueba contra un punto final en vivo aún debería proporcionar estos.

Algunos inconvenientes para probar contra un punto final en vivo:

  • Configuración del entorno: configurar y estandarizar el entorno de ejecución de prueba es más trabajo, y configuraciones de entorno sutilmente diferentes podrían dar como resultado un comportamiento sutilmente diferente
  • Apatridia: trabajar contra un punto final en vivo puede terminar promoviendo pruebas de escritura que se basan en un estado de punto final mutante, que es frágil y difícil de razonar (es decir, cuando algo falla, ¿falla debido a un estado extraño?)
  • El entorno de ejecución de prueba es frágil: si una prueba falla, ¿es la prueba, el código o el punto final en vivo?
  • Velocidad de ejecución: un punto final en vivo suele ser más lento y, a veces, es más difícil de paralelizar
  • Creación de casos límite para pruebas: generalmente triviales con una simulación, a veces un dolor con un punto final en vivo (por ejemplo, los difíciles de configurar son los errores de transporte / HTTP)

Si estuviera en esta situación, y los inconvenientes no parecieran ser un problema, mientras que burlarme del punto final ralentizó considerablemente mi escritura de prueba, probaría contra un punto final en vivo en un instante, siempre y cuando me asegure de verifique nuevamente después de un tiempo para ver que los inconvenientes no resultan ser un problema en la práctica.

orip
fuente
1

Desde la perspectiva de la prueba, hay algunos requisitos que son imprescindibles:

  • Las pruebas (unidad o de otro tipo) nunca deben tener una forma de tocar los datos de producción
  • Los resultados de una prueba nunca deben afectar los resultados de otra prueba
  • Siempre debes comenzar desde una posición conocida

Ese es un gran desafío cuando se conecta a cualquier fuente que mantenga el estado fuera de sus pruebas. No es TDD "puro", pero el equipo de Ruby on Rails resolvió este problema de una manera que podría adaptarse para sus propósitos. El marco de prueba de rails funcionó de esta manera:

  • La configuración de prueba se seleccionó automáticamente al ejecutar pruebas unitarias
  • La base de datos se creó e inicializó al comienzo de las pruebas unitarias en ejecución
  • La base de datos se eliminó después de ejecutar las pruebas unitarias.
  • Si usa SqlLite, la configuración de prueba usa una base de datos RAM

Todo este trabajo se incorporó al arnés de prueba y funciona razonablemente bien. Hay mucho más, pero los conceptos básicos son suficientes para esta conversación.

En los diferentes equipos con los que he trabajado a lo largo del tiempo, tomaríamos decisiones que promoverían la prueba del código, incluso si no fuera la ruta más correcta. Idealmente, envolveríamos todas las llamadas a un almacén de datos con el código que controlamos. En teoría, si alguno de estos viejos proyectos obtuviera nuevos fondos, podríamos retroceder y pasar de la base de datos vinculada a Hadoop al concentrar nuestra atención en solo un puñado de clases.

Los aspectos importantes son no meterse con los datos de producción y asegurarse de que realmente está probando lo que cree que está probando. Es realmente importante poder restablecer el servicio externo a una línea base conocida bajo demanda, incluso desde su código.

Berin Loritsch
fuente