2011-01-07 18 views
9

Nuestro equipo está en el proceso de facilitar el uso de TDD y luchar contra las mejores prácticas para las pruebas unitarias. Nuestro código bajo prueba usa inyección de dependencia. Nuestras pruebas generalmente siguen el tipo de disposición Arrange-Act-Assert donde nos burlamos de las dependencias en la sección Arrange con Moq.Se burlan reutilizables frente a la burla en cada prueba

Teóricamente, las pruebas unitarias deben ser un escudo que lo proteja cuando refactorice. Pero se está convirtiendo en un ancla que nos impide hacerlo. Estoy tratando de determinar dónde está nuestra falla en el proceso.

Considere el ejemplo simplificado:

  • XRepository.Save tiene su firma y el comportamiento/contrato cambió.
  • XController.Save utiliza XRepository.Save para que se refactorice para usar la nueva interfaz. Pero externamente su contrato público no ha cambiado.

Yo esperaría que las pruebas de controlador de OD no necesitará ser reprogramado, pero en lugar de probar a mí que mi nuevo controlador de implementación honores el contrato sin cambios. Pero hemos fallado aquí ya que este no es el caso.

Cada prueba de controlador se burla de la interfaz del repositorio sobre la marcha. Todos ellos necesitan ser cambiados. Además, dado que cada prueba no quiere simular todas las interfaces y métodos, nuestra prueba se relaciona con la implementación particular porque necesita saber qué métodos utilizar para simular.

¡Se vuelve exponencialmente más difícil refactorizar para más pruebas que tenemos! O más exactamente, cuantas más veces nos burlemos de una interfaz.

Así que mis preguntas:

  1. Cualquier preferencia por el uso de la marcha se burla en cada prueba vs haciendo una maqueta hecha a mano reutilizable para cada interfaz?

  2. Dada mi historia, ¿me estoy perdiendo algún principio o estoy cayendo en un escollo común?

Gracias!

+2

Siempre que necesite burlarse de algo, primero piense si puede crear burlas a mano. A menudo encontrará que la simulación sobre la marcha suele dar un olor de complejidad manifiesta. Si ese es el caso, cree simulacros manuales, vaya a los simulacros sobre la marcha –

+1

Algo a considerar: http://en.wikipedia.org/wiki/Interface_segregation_principle – TrueWill

Respuesta

9

No falta ningún principio, pero es un problema común. Creo que cada equipo lo resuelve (o no) a su manera.

Efectos secundarios

Usted seguirá teniendo este problema con cualquier función que tiene efectos secundarios. Me he encontrado para funciones de efectos secundarios que tienen que hacer las pruebas que aseguran algunos o todos de los siguientes:

  • Que fue/no fue llamado
  • El número de veces que se llamó
  • ¿Qué argumentos eran pasado a él
  • Orden de las llamadas.

Asegurar esto en la prueba generalmente significa violar la encapsulación (yo interactúo y sé con la implementación). Cada vez que haga esto, siempre vinculará implícitamente la prueba a la implementación. Esto hará que tenga que actualizar la prueba cada vez que actualice las porciones de implementación que está exponiendo/probando.

reutilizable Mocks

He usado burla reutilizables con gran efecto. El intercambio es su implementación más compleja porque necesita ser más completa. Usted mitiga el costo de actualización de pruebas para acomodar refactores.

Aceptación TDD

Otra opción es cambiar lo que su prueba para. Dado que esto se trata realmente de cambiar su estrategia de prueba, no es algo para entrar a la ligera. Es posible que desee hacer un pequeño análisis primero y ver si realmente sería adecuado para su situación.

Solía ​​hacer TDD con pruebas unitarias. Me encontré con un problema que sentí que no debería haber tenido que enfrentar. Específicamente alrededor de los refactores noté que generalmente teníamos que actualizar muchas pruebas. Estos refactores no estaban dentro de una unidad de código, sino más bien la reestructuración de los componentes principales. Sé que mucha gente dirá que el problema fueron los grandes cambios frecuentes, no las pruebas unitarias. Probablemente hay algo de verdad en los grandes cambios que son parcialmente el resultado de nuestra planificación/arquitectura. Sin embargo, también influyeron las decisiones comerciales que causaron cambios en las direcciones. Estas y otras causas legítimas tuvieron el efecto de necesitar grandes cambios en el código. El resultado final fue que los refactores grandes se volvieron más lentos y dolorosos como resultado de todas las actualizaciones de prueba.

También tropezamos con errores debido a problemas de integración que las pruebas unitarias no cubrían. Hicimos algunas pruebas de aceptación manual. De hecho, hicimos bastante trabajo para que las pruebas de aceptación fueran lo más sencillas posibles. Seguían siendo manuales, y sentimos que había tantas diferencias entre las pruebas unitarias y la prueba de aceptación que debería haber una manera de mitigar el costo de implementar ambas.

Luego la empresa tuvo despidos. De repente, no teníamos la misma cantidad de recursos para invertir en programación y mantenimiento. Nos obligaron a obtener el mayor rendimiento por todo lo que hicimos, incluidas las pruebas. Comenzamos agregando lo que llamamos pruebas de pila parcial para cubrir los problemas de integración comunes que teníamos. Resultó ser tan efectivo que comenzamos a hacer menos pruebas unitarias clásicas. También nos deshicimos de las pruebas de aceptación manual (Selenium). Lentamente avanzamos hacia donde las pruebas comenzaron a probarse hasta que esencialmente estábamos haciendo pruebas de aceptación, pero sin el navegador. Simulamos un método GET, POST o PUT para un controlador en particular y verificamos los criterios de aceptación.

  • La base de datos se actualiza correctamente
  • El código de estado HTTP correcto se volvió
  • Una página fue devuelto que:
    • fue Valid HTML 4.01 estricto
    • contenía la información que queríamos para enviar volver al usuario

Terminamos teniendo menos errores. Específicamente, casi todos los errores de integración y errores debido a refactores grandes desaparecieron casi por completo.

Hubo compensaciones. Resultó que los profesionales superan con creces los inconvenientes de la situación. Contras:

  • La prueba fue generalmente más complicada, y casi todo el mundo prueba algunos efectos secundarios.
  • Podemos decir cuando algo se rompe, pero no es tan específico como las pruebas de la unidad, por lo que tenemos que hacer más depuraciones para rastrear dónde está el problema.
+0

Gracias. Creo que vamos en la misma dirección. Actualmente tenemos pruebas de integración que van desde objetos de nivel de dominio hacia abajo: no llegan tan lejos como para probar los controladores directamente. Los controladores todavía se prueban burlándose de sus dependencias. Gracias por los comentarios. –

1

He luchado con este tipo de problema yo mismo y no tengo una respuesta que me parezca sólida, pero aquí una forma tentativa de pensar. Observo dos tipos de pruebas unitarias

  1. Existen pruebas donde se ejerce la interfaz pública, estas son muy importantes si queremos refactorizar con confianza, demuestran que cumplimos nuestro contrato con nuestros clientes. Estas pruebas son mejor atendidas por un simulacro reutilizable hecho a mano que trata con un pequeño subconjunto de datos de prueba.
  2. Hay pruebas de "cobertura". Estos tienden a ser para demostrar que nuestra implementación se comporta correctamente cuando las dependencias se portan mal. Estos creo que necesitan sobre la marcha burlas para provocar caminos de implementación particulares.
+0

Gracias por el comentario. Valida algunos de mis pensamientos. Creo que estaba tratando de centrarme más en las pruebas de contrato y pensar que si una ruta de código no cumplía parte del contrato era superfluo. Pero tal vez necesitamos ser más pragmáticos. –

Cuestiones relacionadas