Prácticas recomendadas de desarrollo basado en pruebas con C # y RhinoMocks [cerrado]

86

Para ayudar a mi equipo a escribir código comprobable, se me ocurrió esta sencilla lista de mejores prácticas para hacer que nuestra base de código C # sea más comprobable. (Algunos de los puntos se refieren a las limitaciones de Rhino Mocks, un marco de burla para C #, pero las reglas también pueden aplicarse de manera más general). ¿Alguien tiene alguna de las mejores prácticas que siga?

Para maximizar la capacidad de prueba del código, siga estas reglas:

  1. Escribe la prueba primero, luego el código. Razón: esto asegura que usted escriba código comprobable y que cada línea de código obtenga pruebas escritas para él.

  2. Diseñar clases mediante la inyección de dependencias. Razón: No puedes burlarte o probar lo que no se ve.

  3. Separe el código de la interfaz de usuario de su comportamiento utilizando Model-View-Controller o Model-View-Presenter. Motivo: permite probar la lógica empresarial mientras se minimizan las partes que no se pueden probar (la interfaz de usuario).

  4. No escriba métodos o clases estáticos. Motivo: Los métodos estáticos son difíciles o imposibles de aislar y Rhino Mocks no puede burlarse de ellos.

  5. Programe fuera de interfaces, no de clases. Razón: el uso de interfaces aclara las relaciones entre los objetos. Una interfaz debe definir un servicio que un objeto necesita de su entorno. Además, las interfaces se pueden burlar fácilmente con Rhino Mocks y otros marcos de burla.

  6. Aislar las dependencias externas. Razón: No se pueden probar las dependencias externas no resueltas.

  7. Marque como virtuales los métodos que desea simular. Motivo: Rhino Mocks no puede simular métodos no virtuales.

Kevin Albrecht
fuente
Esta es una lista útil. Actualmente estamos usando NUnit y Rhino.Mocks, y es bueno explicar estos criterios para los miembros del equipo que están menos familiarizados con este lado de las pruebas unitarias.
Chris Ballard

Respuestas:

58

Definitivamente una buena lista. Aquí hay algunos pensamientos al respecto:

Escribe la prueba primero, luego el código.

Estoy de acuerdo, a un alto nivel. Pero, sería más específico: "Primero escriba una prueba, luego escriba el código suficiente para aprobar la prueba y repita". De lo contrario, tendría miedo de que mis pruebas unitarias se parezcan más a pruebas de integración o aceptación.

Diseñar clases usando la inyección de dependencia.

Convenido. Cuando un objeto crea sus propias dependencias, no tienes control sobre ellas. La inversión de control / inyección de dependencia le da ese control, lo que le permite aislar el objeto bajo prueba con simulacros / stubs / etc. Así es como prueba los objetos de forma aislada.

Separe el código de la interfaz de usuario de su comportamiento utilizando Model-View-Controller o Model-View-Presenter.

Convenido. Tenga en cuenta que incluso el presentador / controlador se puede probar usando DI / IoC, entregándole una vista y modelo stubned / simulado. Consulte Presenter First TDD para obtener más información al respecto.

No escriba métodos o clases estáticos.

No estoy seguro de estar de acuerdo con este. Es posible realizar una prueba unitaria de un método / clase estática sin usar simulacros. Entonces, tal vez esta sea una de esas reglas específicas de Rhino Mock que mencionaste.

Programe fuera de interfaces, no de clases.

Estoy de acuerdo, pero por una razón ligeramente diferente. Las interfaces brindan una gran flexibilidad al desarrollador de software, más allá del simple soporte para varios marcos de objetos simulados. Por ejemplo, no es posible admitir DI correctamente sin interfaces.

Aislar las dependencias externas.

Convenido. Oculte las dependencias externas detrás de su propia fachada o adaptador (según corresponda) con una interfaz. Esto le permitirá aislar su software de la dependencia externa, ya sea un servicio web, una cola, una base de datos u otra cosa. Esto es especialmente importante cuando su equipo no controla la dependencia (también conocida como externa).

Marque como virtuales los métodos que pretende simular.

Esa es una limitación de Rhino Mocks. En un entorno que prefiere códigos auxiliares codificados a mano sobre un marco de objeto simulado, eso no sería necesario.

Y, un par de puntos nuevos a considerar:

Utilice patrones de diseño de creación. Esto ayudará con DI, pero también le permitirá aislar ese código y probarlo independientemente de otra lógica.

Escribe pruebas usando la técnica Arrange / Act / Assert de Bill Wake . Esta técnica deja muy claro qué configuración es necesaria, qué se está probando realmente y qué se espera.

No tenga miedo de lanzar sus propias imitaciones / talones. A menudo, encontrará que el uso de marcos de objetos simulados hace que sus pruebas sean increíblemente difíciles de leer. Al lanzar el suyo, tendrá control total sobre sus simulacros / talones y podrá mantener sus pruebas legibles. (Refiérase al punto anterior).

Evite la tentación de refactorizar la duplicación de sus pruebas unitarias en clases base abstractas o métodos de instalación / desmontaje. Al hacerlo, se oculta el código de configuración / limpieza del desarrollador que intenta asimilar la prueba unitaria. En este caso, la claridad de cada prueba individual es más importante que refactorizar la duplicación.

Implementar la integración continua. Registre su código en cada "barra verde". Cree su software y ejecute su conjunto completo de pruebas unitarias en cada registro. (Claro, esta no es una práctica de codificación, per se; pero es una herramienta increíble para mantener su software limpio y completamente integrado).

aridlehoover
fuente
3
Por lo general, encuentro que si una prueba es difícil de leer, no es culpa del marco sino del código que está probando. Si el SSP es complicado de configurar, entonces tal vez debería dividirse en más conceptos.
Steve Freeman
10

Si está trabajando con .Net 3.5, es posible que desee buscar en la biblioteca de burla de Moq : utiliza árboles de expresión y lambdas para eliminar el lenguaje no intuitivo de respuesta de registro de la mayoría de las otras bibliotecas de burla.

Consulte esta guía de inicio rápido para ver cuánto más intuitivos se vuelven sus casos de prueba, aquí hay un ejemplo simple:

// ShouldExpectMethodCallWithVariable
int value = 5;
var mock = new Mock<IFoo>();

mock.Expect(x => x.Duplicate(value)).Returns(() => value * 2);

Assert.AreEqual(value * 2, mock.Object.Duplicate(value));
zadam
fuente
5
Creo que la nueva versión de Rhino Mocks también funciona así
George Mauer
3

¡Este es un post muy útil!

Agregaría que siempre es importante comprender el contexto y el sistema bajo prueba (SUT). Seguir los principios de TDD al pie de la letra es mucho más fácil cuando escribe código nuevo en un entorno donde el código existente sigue los mismos principios. Pero cuando escribe código nuevo en un entorno heredado que no es TDD, descubre que sus esfuerzos de TDD pueden aumentar rápidamente mucho más allá de sus estimaciones y expectativas.

Para algunos de ustedes, que viven en un mundo completamente académico, los plazos y la entrega pueden no ser importantes, pero en un entorno donde el software es dinero, hacer un uso efectivo de su esfuerzo TDD es fundamental.

TDD está muy sujeto a la ley de rendimiento marginal decreciente . En resumen, sus esfuerzos hacia TDD son cada vez más valiosos hasta que alcanza un punto de rendimiento máximo, después del cual, el tiempo posterior invertido en TDD tiene cada vez menos valor.

Tiendo a creer que el valor principal de TDD está en los límites (caja negra), así como en las pruebas ocasionales de caja blanca de las áreas críticas del sistema.


fuente
2

La verdadera razón para programar contra interfaces no es facilitarle la vida a Rhino, sino aclarar las relaciones entre los objetos en el código. Una interfaz debe definir un servicio que un objeto necesita de su entorno. Una clase proporciona una implementación particular de ese servicio. Lea el libro "Diseño de objetos" de Rebecca Wirfs-Brock sobre roles, responsabilidades y colaboradores.

Steve Freeman
fuente
De acuerdo ... voy a actualizar mi pregunta para reflejar eso.
Kevin Albrecht
1

Buena lista. Una de las cosas que quizás quieras establecer, y no puedo darte muchos consejos, ya que recién estoy empezando a pensar en ello, es cuándo una clase debería estar en una biblioteca, espacio de nombres, espacios de nombres anidados diferentes. Incluso es posible que desee averiguar una lista de bibliotecas y espacios de nombres de antemano y ordenar que el equipo se reúna y decida fusionar dos / agregar uno nuevo.

Oh, solo pensé en algo que hago y que tú también quieras. Generalmente tengo una biblioteca de pruebas unitarias con un accesorio de prueba por política de clase donde cada prueba entra en un espacio de nombres correspondiente. También suelo tener otra biblioteca de pruebas (¿pruebas de integración?) Que tiene un estilo más BDD . Esto me permite escribir pruebas para especificar qué debería hacer el método y qué debería hacer la aplicación en general.

George Mauer
fuente
También hago una sección de prueba de estilo BDD similar (además del código de prueba de unidad) en un proyecto personal.
Kevin Albrecht
0

Aquí hay otro que pensé que me gusta hacer.

Si planea ejecutar pruebas desde la interfaz gráfica de usuario de prueba unitaria en lugar de desde TestDriven.Net o NAnt, entonces me ha resultado más fácil configurar el tipo de proyecto de prueba unitaria para la aplicación de consola en lugar de la biblioteca. Esto le permite ejecutar pruebas manualmente y recorrerlas paso a paso en modo de depuración (que el TestDriven.Net mencionado anteriormente puede hacer por usted).

Además, siempre me gusta tener un proyecto de Playground abierto para probar fragmentos de código e ideas con las que no estoy familiarizado. Esto no debe registrarse en el control de fuente. Aún mejor, debería estar en un repositorio de control de fuente separado solo en la máquina del desarrollador.

George Mauer
fuente