Está creando los objetos que cree que necesitará en una primera prueba en TDD

15

Soy bastante nuevo en TDD y tengo problemas al crear mi primera prueba cuando se trata de cualquier código de implementación. Sin ningún marco para el código de implementación, soy libre de escribir mi primera prueba como quiera, pero siempre parece estar contaminada por mi forma de pensar Java / OO sobre el problema.

Por ejemplo, en mi Github ConwaysGameOfLifeExample, la primera prueba que escribí (rule1_zeroNeighbours) comencé creando un objeto GameOfLife que aún no se había implementado; llamó un método de conjunto que no existía, un método de paso que no existía, un método de obtención que no existía, y luego usó una afirmación.

Las pruebas evolucionaron a medida que escribía más pruebas y las refactorizaba, pero originalmente se parecía a esto:

@Test
public void rule1_zeroNeighbours()
{
    GameOfLife gameOfLife = new GameOfLife();
    gameOfLife.set(1, 1, true);
    gameOfLife.step();
    assertEquals(false, gameOfLife.get(1, 1));
}

Esto se sintió extraño ya que estaba forzando el diseño de la implementación en función de cómo había decidido en esta etapa temprana escribir esta primera prueba.

En la forma en que entiendes TDD, ¿está bien? Parece que sigo los principios de TDD / XP en el sentido de que mis pruebas e implementación evolucionaron con el tiempo con la refactorización, por lo que si este diseño inicial hubiera resultado inútil, habría estado abierto a cambios, pero parece que estoy forzando una dirección sobre el solución comenzando de esta manera.

¿De qué otra forma las personas usan TDD? Podría haber pasado por más iteraciones de refactorización comenzando sin ningún objeto GameOfLife, solo primitivos y métodos estáticos, pero eso parece demasiado artificial.

Encaitar
fuente
55
TDD no reemplaza ni la planificación cuidadosa ni la selección cuidadosa del patrón de diseño. Dicho esto, antes de escribir cualquier implementación para satisfacer las primeras líneas de prueba, es un momento mucho mejor que después de escribir dependencias para reconocer que su plan es estúpido, que ha elegido el patrón incorrecto, o incluso simplemente que es incómodo o confuso invocar una clase de la manera que exige su examen.
svidgen

Respuestas:

9

Esto se sintió extraño ya que estaba forzando el diseño de la implementación en función de cómo había decidido en esta etapa temprana escribir esta primera prueba.

Creo que este es el punto clave en su pregunta, si esto es deseable o no depende de si se inclina hacia la idea de Codeninja de que debe diseñar por adelantado y luego usa TDD para completar la implementación, o la idea de Durron de que las pruebas deben participar eliminando el diseño y la implementación.

Creo que cuál de estos prefiere (o dónde cae en el medio) es algo que necesita descubrir por sí mismo como una preferencia. Es útil comprender los pros y los contras de cada enfoque. Probablemente hay muchos, pero diría que los principales son:

Diseño profesional por adelantado

  • Por muy bueno que sea el proceso de TDD en el diseño de conducción, no es perfecto. Hacer TD sin un destino concreto en mente a veces puede acabar en callejones sin salida, y al menos algunos de estos callejones sin salida podrían haberse evitado con un poco de pensamiento inicial sobre dónde quiere terminar. Este artículo de blog presenta este argumento utilizando el ejemplo del kata de los números romanos, y tiene una implementación final bastante agradable para mostrar.

Diseño de prueba de conducción profesional

  • Al construir su implementación alrededor de un cliente de su código (sus pruebas), obtiene la adherencia a YAGNI de forma gratuita, siempre y cuando no comience a escribir casos de prueba innecesarios. En términos más generales, obtienes una API que está diseñada en torno a su uso por parte de un consumidor, que en última instancia es lo que deseas.

  • La idea de dibujar un montón de diagramas UML antes de escribir cualquier código y luego completar los huecos es agradable, pero rara vez es realista. En el Código completo de Steve McConnell, el diseño se describe como un "problema perverso", un problema que no se puede entender completamente sin primero resolverlo al menos parcialmente. Combine esto con el hecho de que el problema subyacente en sí mismo puede cambiar a través de los requisitos cambiantes, y este modelo de diseño comienza a sentirse un poco desesperado. La prueba de manejo le permite comenzar una parte del trabajo a la vez, en el diseño, no solo en la implementación, y saber que, al menos durante la vida útil de cambiar de rojo a verde, esa tarea aún estará actualizada y será relevante.

En cuanto a su ejemplo en particular, como dice Durron, si optara por llevar a cabo el diseño escribiendo la prueba más simple, consumiendo la interfaz mínima posible, probablemente comenzaría con una interfaz más simple que la de su fragmento de código .

Ben Aaronson
fuente
El enlace fue una muy buena lectura de Ben. Gracias por compartir eso.
RubberDuck
1
@RubberDuck De nada! No estoy completamente de acuerdo con eso, en realidad, pero creo que hace un excelente trabajo argumentando ese punto de vista.
Ben Aaronson
1
No estoy seguro de hacerlo tampoco, pero hace que su caso sea bueno. Creo que la respuesta correcta está en algún punto intermedio. Debe tener un plan, pero ciertamente si sus pruebas se sienten incómodas, rediseñe. De todos modos ... ++ buen espectáculo viejo frijol.
RubberDuck
17

Para escribir la prueba en primer lugar, debe diseñar la API que luego va a implementar. Ya ha comenzado con el pie equivocado escribiendo su prueba para crear el objeto completo GameOfLife y usándola para implementar su prueba.

De Pruebas unitarias prácticas con JUnit y Mockito :

Al principio puede que te sientas incómodo al escribir algo que ni siquiera está allí. Requiere un ligero cambio en sus hábitos de codificación, pero después de un tiempo verá que es una gran oportunidad de diseño. Al escribir las pruebas primero, tiene la posibilidad de crear una API que sea conveniente para un cliente. Su prueba es el primer cliente de una API recién nacida. De esto se trata realmente TDD: el diseño de una API.

Su prueba no intenta en gran medida diseñar una API. Ha configurado un sistema con estado donde toda la funcionalidad está contenida en el exteriorGameOfLife clase .

Si escribiera esta aplicación, pensaría en las piezas que quiero construir. Por ejemplo, podría hacer una Cellclase, escribir pruebas para eso, antes de pasar a la aplicación más grande. Ciertamente, crearía una clase para la estructura de datos "infinito en todas las direcciones" que se requiere para implementar adecuadamente Conway, y probarlo. Una vez hecho todo eso, pensaría en escribir la clase general que tiene unmain método, etc.

Es fácil pasar por alto el paso "escribir una prueba que falla". Pero escribir la prueba de falla que funciona de la manera que usted desea es el núcleo de TDD.

durron597
fuente
1
Teniendo en cuenta que una celda solo sería un envoltorio para un boolean, ese diseño ciertamente sería peor para el rendimiento. ¿A menos que necesite ser extensible en el futuro a otros autómatas celulares con más de dos estados?
user253751
2
@immibis Eso es una discusión sobre los detalles. Podría comenzar con una clase que represente la colección de celdas. También podría migrar / fusionar la clase de celda y sus pruebas con una clase que representa una colección de celdas más adelante si el rendimiento es un problema.
Eric
@immibis El número de vecinos vivos podría almacenarse por razones de rendimiento. Número de garrapatas que la célula ha estado viva, por razones de color ...
Blorgbeard sale el
La optimización prematura de @immibis es el mal ... Además, evitar la obsesión primitiva es la mejor manera de escribir código de buena calidad, sin importar cuántos estados admita. Eche un vistazo a: jamesshore.com/Blog/PrimitiveObsession.html
Paul
0

Hay diferentes escuelas de pensamiento sobre esto.

Algunos dicen: la prueba no se compila es un error: vaya a arreglar escriba el código de producción más pequeño disponible.

Algunos dicen: está bien escribir la prueba primero, verificar si apesta (o no) y luego crear clases / métodos faltantes

Con el primer enfoque, realmente estás en un ciclo rojo-verde-refactorizador. Con el segundo, tiene una visión un poco más amplia de lo que desea lograr.

Depende de usted elegir la forma en que trabaja. En mi humilde opinión, ambos enfoques son válidos.

timoras
fuente
0

Incluso cuando implemente algo de forma "pirateada", sigo pensando en las clases y los pasos que estarán involucrados en todo el programa. Así que has pensado esto y escrito estos pensamientos de diseño como prueba primero, ¡eso es genial!

Ahora siga iterando a través de ambas implementaciones para completar esta prueba inicial, y luego agregue más pruebas para mejorar y extender el diseño.

Lo que podría ayudarlo es usar Pepino o similar para escribir sus pruebas.

gbjbaanb
fuente
0

Antes de comenzar a escribir sus pruebas, debe pensar en cómo diseñar su sistema. Debe pasar una cantidad considerable de tiempo durante su fase de diseño. Si lo hizo, no obtendrá esta confusión sobre TDD.

TDD es solo un enlace de enfoque de desarrollo : TDD
1. Agregue una prueba
2. Ejecute todas las pruebas y vea si la nueva falla
3. Escriba algún código
4. Ejecute pruebas
5. Código de refactorización
6. Repita

TDD lo ayuda a cubrir todas las características requeridas de lo que ha planeado antes de comenzar a desarrollar su software. enlace: Beneficios

codeninja.sj
fuente
0

No me gustan las pruebas de nivel de sistema escritas en Java o C # por esa razón. Mire SpecFlow para c # o uno de los marcos de prueba basados ​​en Cucumber para java (quizás JBehave). Entonces sus pruebas pueden verse más así.

ingrese la descripción de la imagen aquí

Y puede cambiar el diseño de su objeto sin tener que cambiar todas las pruebas de su sistema.

(Las pruebas unitarias "normales" son excelentes cuando se prueban clases individuales).

¿Cuáles son las diferencias entre los marcos BDD para Java?

Ian
fuente