¿Cuándo escribe el código "real" en TDD?

146

Todos los ejemplos que he leído y visto en videos de capacitación tienen ejemplos simplistas. Pero lo que no veo es cómo hago el código "real" después de obtener verde. ¿Es esta la parte "Refactor"?

Si tengo un objeto bastante complejo con un método complejo, y escribo mi prueba y lo mínimo para hacerla pasar (después de que falla, Rojo). ¿Cuándo vuelvo y escribo el código real? ¿Y cuánto código real escribo antes de volver a probar? Supongo que la última es más intuición.

Editar: Gracias a todos los que respondieron. Todas sus respuestas me ayudaron inmensamente. Parece que hay diferentes ideas sobre lo que preguntaba o confundía, y tal vez las hay, pero lo que preguntaba era, digamos que tengo una solicitud para construir una escuela.

En mi diseño, tengo una arquitectura con la que quiero comenzar, Historias de usuarios, etc. A partir de aquí, tomo esas Historias de usuario y creo una prueba para probar la Historia de usuario. El usuario dice: "Tenemos personas que se inscriben en la escuela y pagan las cuotas de inscripción". Entonces, pienso en una manera de hacer que eso falle. Al hacerlo, diseño una clase de prueba para la clase X (tal vez Estudiante), que fallará. Entonces creo la clase "Estudiante". Tal vez "Escuela" no lo sé.

Pero, en cualquier caso, el TD Design me obliga a pensar en la historia. Si puedo hacer que una prueba falle, sé por qué falla, pero esto presupone que puedo hacerla pasar. Se trata del diseño.

Me gusta esto al pensar en Recursión. La recursión no es un concepto difícil. Puede que sea más difícil seguirle la pista en la cabeza, pero en realidad, lo más difícil es saber cuándo la recursión se "rompe", cuándo detenerme (mi opinión, por supuesto). Por lo tanto, tengo que pensar en qué se detiene. La Recursión Primero. Es solo una analogía imperfecta, y asume que cada iteración recursiva es un "pase". De nuevo, solo una opinión.

En la implementación, la escuela es más difícil de ver. Los libros de contabilidad numéricos y bancarios son "fáciles" en el sentido de que puede usar aritmética simple. Puedo ver a + b y devolver 0, etc. En el caso de un sistema de personas, tengo que pensar más en cómo implementar eso. Tengo el concepto de fallar, aprobar, refactorizar (principalmente debido al estudio y esta pregunta).

En mi opinión, lo que no sé se basa en la falta de experiencia. No sé cómo dejar de inscribir a un nuevo estudiante. No sé cómo fallar a alguien escribiendo un apellido y guardándolo en una base de datos. Sé cómo hacer un +1 para las matemáticas simples, pero con entidades como una persona, no sé si solo estoy probando para ver si recupero una ID de base de datos única o algo más cuando alguien ingresa un nombre en un base de datos o ambos o ninguno.

O, quizás esto muestre que todavía estoy confundido.

    
pregunta johnny 24.07.2017 - 23:55
fuente

11 respuestas

240
  

Si tengo un objeto bastante complejo con un método complejo, y escribo   mi prueba y lo mínimo para hacerlo pasar (después de que falla por primera vez,   Rojo). ¿Cuándo vuelvo y escribo el código real? Y cuánto   ¿Qué código real escribo antes de volver a probar? Supongo que el último es   Más intuición.

Usted no "vuelve" y escribe "código real". Es todo el código real. Lo que hace es regresar y agregar otra prueba que obligue a cambiar su código para hacer que la nueva prueba pase.

¿Cuánto código escribe antes de volver a realizar la prueba? Ninguna. Usted escribe el código cero sin una prueba fallida que obliga a escribir más código.

¿Observa el patrón?

Vayamos a través de (otro) ejemplo simple con la esperanza de que sirva.

Assert.Equal("1", FizzBuzz(1));

Fácil peazy.

public String FizzBuzz(int n) {
    return 1.ToString();
}

No es lo que llamarías código real, ¿verdad? Añadamos una prueba que obligue a un cambio.

Assert.Equal("2", FizzBuzz(2));

Podríamos hacer algo tonto como if n == 1 , pero nos saltaremos a la solución sana.

public String FizzBuzz(int n) {
    return n.ToString();
}

Genial. Esto funcionará para todos los números que no sean FizzBuzz. ¿Cuál es la siguiente entrada que forzará el cambio del código de producción?

Assert.Equal("Fizz", FizzBuzz(3));

public String FizzBuzz(int n) {
    if (n == 3)
        return "Fizz";
    return n.ToString();
}

Y de nuevo. Escribe una prueba que no pasará todavía.

Assert.Equal("Fizz", FizzBuzz(6));

public String FizzBuzz(int n) {
    if (n % 3 == 0)
        return "Fizz";
    return n.ToString();
}

Y ahora hemos cubierto todos los múltiplos de tres (que no son también múltiplos de cinco, lo notaremos y volveremos).

Todavía no hemos escrito una prueba para "Buzz", así que vamos a escribir eso.

Assert.Equal("Buzz", FizzBuzz(5));

public String FizzBuzz(int n) {
    if (n % 3 == 0)
        return "Fizz";
    if (n == 5)
        return "Buzz"
    return n.ToString();
}

Y de nuevo, sabemos que hay otro caso que debemos manejar.

Assert.Equal("Buzz", FizzBuzz(10));

public String FizzBuzz(int n) {
    if (n % 3 == 0)
        return "Fizz";
    if (n % 5 == 0)
        return "Buzz"
    return n.ToString();
}

Y ahora podemos manejar todos los múltiplos de 5 que no son también múltiplos de 3.

Hasta este punto, hemos estado ignorando el paso de refactorización, pero veo algo de duplicación. Vamos a limpiar eso ahora.

private bool isDivisibleBy(int divisor, int input) {
    return (input % divisor == 0);
}

public String FizzBuzz(int n) {
    if (isDivisibleBy(3, n))
        return "Fizz";
    if (isDivisibleBy(5, n))
        return "Buzz"
    return n.ToString();
}

Genial. Ahora hemos eliminado la duplicación y hemos creado una función bien nombrada. ¿Cuál es la próxima prueba que podemos escribir que nos obligará a cambiar el código? Bueno, hemos estado evitando el caso donde el número es divisible entre 3 y 5. Vamos a escribirlo ahora.

Assert.Equal("FizzBuzz", FizzBuzz(15));

public String FizzBuzz(int n) {
    if (isDivisibleBy(3, n) && isDivisibleBy(5, n))
        return "FizzBuzz";
    if (isDivisibleBy(3, n))
        return "Fizz";
    if (isDivisibleBy(5, n))
        return "Buzz"
    return n.ToString();
}

Las pruebas pasan, pero tenemos más duplicaciones. Tenemos opciones, pero voy a aplicar "Extraer variable local" unas cuantas veces para que estemos refactorizando en lugar de reescribir.

public String FizzBuzz(int n) {

    var isDivisibleBy3 = isDivisibleBy(3, n);
    var isDivisibleBy5 = isDivisibleBy(5, n);

    if ( isDivisibleBy3 && isDivisibleBy5 )
        return "FizzBuzz";
    if ( isDivisibleBy3 )
        return "Fizz";
    if ( isDivisibleBy5 )
        return "Buzz"
    return n.ToString();
}

Y hemos cubierto cada entrada razonable, pero ¿qué pasa con la entrada no razonable ? ¿Qué pasa si pasamos 0 o un negativo? Escribe esos casos de prueba.

public String FizzBuzz(int n) {

    if (n < 1)
        throw new InvalidArgException("n must be >= 1);

    var isDivisibleBy3 = isDivisibleBy(3, n);
    var isDivisibleBy5 = isDivisibleBy(5, n);

    if ( isDivisibleBy3 && isDivisibleBy5 )
        return "FizzBuzz";
    if ( isDivisibleBy3 )
        return "Fizz";
    if ( isDivisibleBy5 )
        return "Buzz"
    return n.ToString();
}

¿Esto está empezando a parecerse a "código real" todavía? Más importante aún, ¿en qué momento dejó de ser un "código irreal" y pasó a ser "real"? Eso es algo para reflexionar sobre ...

Entonces, pude hacer esto simplemente buscando una prueba que sabía que no pasaría en cada paso, pero tenía mucha práctica. Cuando estoy en el trabajo, las cosas no siempre son tan simples y es posible que no siempre sepa qué prueba forzará un cambio. ¡A veces escribo una prueba y me sorprende ver que ya pasa! Le recomiendo que adquiera el hábito de crear una "Lista de prueba" antes de comenzar. Esta lista de prueba debe contener todas las entradas "interesantes" que pueda imaginar. Es posible que no los uses todos y es probable que agregues casos a medida que avanzas, pero esta lista sirve como una hoja de ruta. Mi lista de pruebas para FizzBuzz se vería así.

  • Negativo
  • Cero
  • uno
  • dos
  • tres
  • cuatro
  • cinco
  • Seis (múltiplo no trivial de 3)
  • Nueve (3 al cuadrado)
  • Diez (múltiplo no trivial de 5)
  • 15 (múltiplo de 3 & 5)
  • 30 (múltiplo no trivial de 3 y 5)
respondido por el RubberDuck 25.07.2017 - 04:22
fuente
46

El código "real" es el código que escribes para hacer que tu prueba pase. Realmente . Es así de simple.

Cuando las personas hablan sobre escribir lo mínimo para que la prueba sea verde, eso significa que su código real debe seguir el YAGNI principio .

La idea del paso de refactorización es simplemente limpiar lo que has escrito una vez que estés satisfecho de que cumple con los requisitos.

Siempre que las pruebas que escriba realmente abarquen los requisitos de su producto, una vez que se aprueben, el código estará completo. Piénselo, si todos sus requisitos comerciales tienen una prueba y todas esas pruebas son verdes, ¿qué más hay para escribir? (De acuerdo, en la vida real no tendemos a tener una cobertura de prueba completa, pero la teoría es sólida).

    
respondido por el GenericJon 25.07.2017 - 00:21
fuente
14

La respuesta corta es que el "código real" es el código que hace que la prueba pase. Si puede hacer que su prueba pase con algo que no sea un código real, ¡agregue más pruebas!

Estoy de acuerdo en que muchos tutoriales sobre TDD son simplistas. Eso funciona contra ellos. Una prueba demasiado simple para un método que, por ejemplo, calcula 3 + 8 realmente no tiene más remedio que calcular 3 + 8 y comparar el resultado. Eso hace que parezca que simplemente va a duplicar el código por todas partes, y que las pruebas son trabajos adicionales inútiles y propensos a errores.

Cuando seas bueno en las pruebas, eso te indicará cómo estructuras tu aplicación y cómo escribes tu código. Si tiene problemas para realizar pruebas sensatas y útiles, probablemente debería reconsiderar un poco su diseño. Un sistema bien diseñado es fácil de probar, lo que significa que las pruebas sensibles son fáciles de pensar e implementar.

Cuando primero escribe sus pruebas, las observa fallar y luego escribe el código que las hace pasar, eso es una disciplina para garantizar que todo su código tenga las pruebas correspondientes. No sigo obedientemente esa regla cuando estoy codificando; A menudo escribo pruebas después del hecho. Pero hacer pruebas primero ayuda a mantenerte honesto. Con algo de experiencia, comenzarás a notar cuando te codificas en una esquina, incluso cuando no estás escribiendo las pruebas primero.

    
respondido por el Carl Raymond 25.07.2017 - 00:21
fuente
6

A veces, algunos ejemplos sobre TDD pueden ser engañosos. Como otras personas han señalado anteriormente, el código que escribes para hacer pasar las pruebas es el código real.

Pero no creas que el código real aparece como magia, eso es incorrecto. Necesita una mejor comprensión de lo que quiere lograr y luego debe elegir la prueba en consecuencia, a partir de los casos más sencillos y de esquina.

Por ejemplo, si necesitas escribir un lexer, comienzas con una cadena vacía, luego con un montón de espacios en blanco, luego un número, luego con un número rodeado de espacios en blanco, luego un número incorrecto, etc. Estas pequeñas transformaciones Diríjase al algoritmo correcto, pero no salta del caso más sencillo al de un caso altamente complejo, que se elige sin hacer ruido para obtener el código real.

Bob Martin lo explica perfectamente aquí .

    
respondido por el Victor Cejudo 25.07.2017 - 09:24
fuente
5

La parte del refactor se limpia cuando estás cansado y quieres ir a casa.

Cuando está a punto de agregar una característica, la parte de refactor es lo que cambia antes de la próxima prueba. Usted refactoriza el código para dejar espacio para la nueva característica. Haces esto cuando sabes cuál será la nueva característica. No cuando solo lo estas imaginando.

Esto puede ser tan simple como cambiar el nombre de GreetImpl a GreetWorld antes de crear una clase GreetMom (después de agregar una prueba) para agregar una función que imprima "Hola mamá".

    
respondido por el candied_orange 25.07.2017 - 03:04
fuente
1

Pero el código real aparecería en la etapa de refactor de la fase TDD. Es decir. el código que debe ser parte de la versión final.

Las pruebas se deben ejecutar cada vez que realice un cambio.

El lema del ciclo de vida de TDD sería: REFACTOR VERDE ROJO

RED : escribe las pruebas

VERDE : realice un intento honesto de obtener un código funcional que pase las pruebas lo más rápido posible: código duplicado, variables de nombres ocultos, hacks del orden más alto, etc.

REFACTOR : limpie el código, nombre correctamente las variables. DRY el código.

    
respondido por el graeme 25.07.2017 - 06:51
fuente
1
  

¿Cuándo escribes el código "real" en TDD?

La fase roja es donde escribe el código.

En la fase refactorización , el objetivo principal es eliminar código.

En la fase roja usted hace cualquier cosa para que la prueba pase lo más rápido posible y a cualquier costo . Usted ignora por completo lo que alguna vez escuchó acerca de las buenas prácticas de codificación o el patrón de diseño. Hacer que la prueba sea verde es todo lo que importa.

En la fase refactorización usted limpia el desastre que acaba de crear. Ahora mire primero si el cambio que acaba de hacer es el tipo de la parte superior de la lista de Prioridad de transformación y si hay cualquier duplicación de código que pueda eliminar lo más probable es que aplique un patrón de diseño.

Finalmente, mejora la legibilidad al cambiar el nombre de los identificadores y extrae números mágicos y / o cadenas literales a constantes.

  

No es refactor rojo, es refactor rojo-verde. - Rob Kinyon

Gracias por señalar esto.

Así que es la fase verde donde escribes el código real

En la fase roja escribe la especificación ejecutable ...

    
respondido por el Timothy Truckle 26.07.2017 - 09:52
fuente
1

Estás escribiendo Real Code todo el tiempo.

En cada paso, está escribiendo un código para satisfacer las condiciones que Su código satisfará para los futuros llamantes de Su código (que puede ser Usted o no ...).

Crees que no estás escribiendo un código útil ( real ), porque en un momento podrías refactorizarlo.

  

Refactorización de código    es el proceso de reestructuración del código de computadora existente, cambiando la factorización, sin cambiar su comportamiento externo.

Lo que esto significa es que aunque esté cambiando el código, las condiciones que satisface el código se mantienen sin cambios. Y las verificaciones ( pruebas ) que implementó para verificar que su código ya está allí para verificar si sus modificaciones han cambiado algo. Así que el código que escribiste todo el tiempo está ahí, de una manera diferente.

Otra razón por la que puedes pensar que no es un código real, es que estás haciendo ejemplos en los que el programa final ya puede ser visto por ti. Esto es muy bueno, ya que muestra que tienes conocimiento sobre el dominio en el que estás programando.
Pero muchas veces los programadores están en un dominio que es nuevo , desconocido para ellos. No saben cuál será el resultado final y TDD es una técnica para escribir programas paso a paso, documentando nuestro conocimiento sobre cómo debería funcionar este sistema y verificando que nuestros El código funciona de esa manera.

Cuando leí El libro (*) en TDD, para mí, la característica más importante que se destacó fue la lista: TODO. Me mostró que, TDD también es una técnica para ayudar a los desarrolladores a enfocarse en una cosa a la vez. Entonces, esta es también una respuesta a Su pregunta sobre ¿Cuánto código real para escribir ? Yo diría código suficiente para enfocarme en una cosa a la vez.

(*) "Test Driven Development: By Example" por Kent Beck

    
respondido por el Robert Andrzejuk 27.07.2017 - 22:43
fuente
1

No estás escribiendo código para hacer que tus pruebas fallen.

Escribes tus pruebas para definir cómo debería ser el éxito, lo que inicialmente debería fallar porque aún no has escrito el código que pasará.

El punto principal sobre escribir pruebas que fallan inicialmente es hacer dos cosas:

  1. Cubrir todos los casos: todos los casos nominales, todos los casos de borde, etc.
  2. Valide sus pruebas. Si solo los ve pasar, ¿cómo puede estar seguro de que informarán de manera confiable de una falla cuando ocurra?

El punto detrás de refactor rojo-verde es que escribir primero las pruebas correctas le da la confianza de saber que el código que escribió para aprobar las pruebas es correcto y le permite refactorizar con la confianza de que sus pruebas le informarán tan pronto como algo se rompe, para que pueda volver inmediatamente y repararlo.

En mi propia experiencia (C # / .NET), puro test-first es un ideal un tanto inalcanzable, porque no puedes compilar una llamada a un método que aún no existe. Así que "probar primero" se trata realmente de codificar interfaces e implementaciones de apéndices primero, luego escribir pruebas contra los apéndices (que inicialmente fallarán) hasta que los apéndices se realicen correctamente. Nunca voy a escribir "código de error", simplemente a partir de talones.

    
respondido por el Zenilogix 31.07.2017 - 05:10
fuente
0

Creo que puede estar confundido entre pruebas de unidad y pruebas de integración. Creo que también puede haber pruebas de aceptación, pero eso depende de su proceso.

Una vez que hayas probado todas las pequeñas "unidades", entonces las pruebas todas ensambladas o "integradas". Eso suele ser un programa completo o una biblioteca.

En el código que he escrito, la integración prueba una biblioteca con varios programas de prueba que leen datos y los envían a la biblioteca, luego verifican los resultados. Luego lo hago con hilos. Luego lo hago con hilos y tenedor () en el medio. Luego lo ejecuto y mato -9 después de 2 segundos, luego lo inicio y compruebo su modo de recuperación. Yo lo peleo. Lo torturo de muchas formas.

Todo eso TAMBIÉN está siendo probado, pero no tengo una pantalla bastante roja / verde para los resultados. O tiene éxito, o busco unas cuantas miles de líneas de código de error para averiguar por qué.

Allí es donde se prueba el "código real".

Y solo pensé en esto, pero tal vez no sabes cuándo se supone que hayas terminado de escribir las pruebas unitarias. Ha terminado de escribir pruebas unitarias cuando sus pruebas ejercitan todo lo que usted especificó que debería hacer. A veces, puede perder el rastro de eso entre todos los casos de manejo de errores y de borde, por lo que es posible que desee hacer un buen grupo de pruebas de pruebas de ruta feliz que simplemente pasen directamente por las especificaciones.

    
respondido por el Zan Lynx 27.07.2017 - 07:55
fuente
-6

En respuesta al título de la pregunta: "¿Cuándo escribes el código" real "en TDD?", la respuesta es: "casi nunca" o "muy lentamente".

Suenas como un estudiante, así que responderé como si aconsejara a un estudiante.

Vas a aprender un montón de "teorías" y "técnicas" de codificación. Son excelentes para pasar el tiempo en cursos para estudiantes sobrevaluados, pero no son de gran beneficio para usted que no pudo leer en un libro en la mitad del tiempo.

El trabajo de un codificador es únicamente producir código. Código que funciona muy bien. Es por eso que usted, el programador, planifica el código en su mente, en un papel, en una aplicación adecuada, etc., y planea solucionar de antemano posibles fallas / agujeros pensando lógicamente y lateralmente antes de codificar.

Pero necesita saber cómo romper su aplicación para poder diseñar código decente. Por ejemplo, si no sabía acerca de Little Bobby Table (xkcd 327), entonces Probablemente no esté limpiando sus entradas antes de trabajar con la base de datos, por lo que no podrá proteger sus datos en torno a ese concepto.

TDD es solo un flujo de trabajo diseñado para minimizar los errores en su código al crear las pruebas de lo que podría salir mal antes de que codifique su aplicación, ya que la codificación se puede volver más complicada cuanto más código introduzca y olvide los errores que alguna vez pensó. . Una vez que crees que has terminado tu aplicación, ejecutas las pruebas y el auge, con suerte los errores se detectan con tus pruebas.

TDD no es, como creen algunas personas, escribir una prueba, pasarla con un código mínimo, escribir otra prueba, hacerla pasar con un código mínimo, etc. En cambio, es una forma de ayudarlo a codificar con confianza. Este ideal de código de refactorización continua para que funcione con las pruebas es idiota, pero es un buen concepto entre los estudiantes porque los hace sentir muy bien cuando agregan una nueva función y aún están aprendiendo a codificar ...

Por favor, no caiga en esta trampa y vea su función de codificación por lo que es: el trabajo de un codificador es únicamente producir código. Código que funciona muy bien. Ahora, recuerde que estará en el reloj como programador profesional, y que a su cliente no le importará si escribió 100,000 afirmaciones, o 0. Solo quieren un código que funcione. Muy bien, de hecho.

    
respondido por el user3791372 25.07.2017 - 22:33
fuente

Lea otras preguntas en las etiquetas