¿Cómo evitar errores lógicos en el código, cuando TDD no ayudó?

67

Recientemente estaba escribiendo un pequeño fragmento de código que indicaría de forma amigable para los humanos la antigüedad de un evento. Por ejemplo, podría indicar que el evento ocurrió "Hace tres semanas" o "Hace un mes" o "Ayer".

Los requisitos eran relativamente claros y este era un caso perfecto para el desarrollo basado en pruebas. Escribí las pruebas una por una, implementando el código para pasar cada prueba, y todo parecía funcionar perfectamente. Hasta que apareció un error en producción.

Aquí está el código relevante:

now = datetime.datetime.utcnow()
today = now.date()
if event_date.date() == today:
    return "Today"

yesterday = today - datetime.timedelta(1)
if event_date.date() == yesterday:
    return "Yesterday"

delta = (now - event_date).days

if delta < 7:
    return _number_to_text(delta) + " days ago"

if delta < 30:
    weeks = math.floor(delta / 7)
    if weeks == 1:
        return "A week ago"

    return _number_to_text(weeks) + " weeks ago"

if delta < 365:
    ... # Handle months and years in similar manner.

Las pruebas verificaban el caso de un evento que ocurría hoy, ayer, hace cuatro días, hace dos semanas, hace una semana, etc., y el código se creó en consecuencia.

Lo que me perdí es que un evento puede ocurrir un día antes de ayer, mientras que fue hace un día: por ejemplo, un evento que sucedió hace veintiséis horas sería hace un día, mientras que no exactamente ayer si ahora es la 1 de la mañana. Más exactamente, es un punto algo, pero como deltaes un número entero, será solo uno. En este caso, la aplicación muestra "Hace un día", que obviamente es inesperado y no se maneja en el código. Se puede solucionar agregando:

if delta == 1:
    return "A day ago"

justo después de calcular el delta.

Si bien la única consecuencia negativa del error es que perdí media hora preguntándome cómo podría suceder este caso (y creyendo que tiene que ver con las zonas horarias, a pesar del uso uniforme de UTC en el código), su presencia me preocupa. Indica que:

  • Es muy fácil cometer un error lógico incluso en un código fuente tan simple.
  • El desarrollo impulsado por pruebas no ayudó.

También preocupante es que no puedo ver cómo se pueden evitar estos errores. Además de pensar más antes de escribir el código, la única forma en que puedo pensar es en agregar muchas afirmaciones para los casos que creo que nunca sucederían (como creía que hace un día es necesariamente ayer), y luego recorrer cada segundo por los últimos diez años, verificando cualquier violación de afirmación, que parece demasiado compleja.

¿Cómo podría evitar crear este error en primer lugar?

Arseni Mourzenko
fuente
38
Al tener un caso de prueba para ello? Parece que lo descubriste después y combina con TDD.
Julurous
63
Acabas de experimentar por qué no soy un fanático del desarrollo basado en pruebas: en mi experiencia, la mayoría de los errores detectados en la producción son escenarios en los que nadie pensó. El desarrollo impulsado por pruebas y las pruebas unitarias no hacen nada por esto. (Sin embargo, las pruebas unitarias tienen valor para detectar errores introducidos a través de ediciones futuras).
Loren Pechtel
102
Repita después de mí: "No hay balas de plata, incluido el TDD". No hay proceso, ni conjunto de reglas, ni algoritmo que pueda seguir robóticamente para producir un código perfecto. Si lo hubiera, podríamos automatizar todo el proceso y terminar de una vez.
jpmc26
43
Felicitaciones, redescubrió la vieja sabiduría de que ninguna prueba puede probar la ausencia de errores. Pero si está buscando técnicas para crear una mejor cobertura del posible dominio de entrada, debe hacer un análisis exhaustivo del dominio, los casos límite y las clases de equivalencia de ese dominio. Todas las técnicas antiguas y bien conocidas, conocidas desde mucho antes de que se inventara el término TDD.
Doc Brown
80
No estoy tratando de ser sarcástico, pero su pregunta aparentemente podría reformularse como "¿cómo pienso en cosas que no pensé?". No estoy seguro de qué tiene que ver eso con TDD.
Jared Smith

Respuestas:

57

Estos son los tipos de errores que normalmente encuentra en el paso de refactorización de rojo / verde / refactorización. ¡No olvides ese paso! Considere un refactor como el siguiente (no probado):

def pluralize(num, unit):
    if num == 1:
        return unit
    else:
        return unit + "s"

def convert_to_unit(delta, unit):
    factor = 1
    if unit == "week":
        factor = 7 
    elif unit == "month":
        factor = 30
    elif unit == "year":
        factor = 365
    return delta // factor

def best_unit(delta):
    if delta < 7:
        return "day"
    elif delta < 30:
        return "week"
    elif delta < 365:
        return "month"
    else:
        return "year"

def human_friendly(event_date):
    date = event_date.date()
    today = now.date()
    yesterday = today - datetime.timedelta(1)
    if date == today:
        return "Today"
    elif date == yesterday:
        return "Yesterday"
    else:
        delta = (now - event_date).days
        unit = best_unit(delta)
        converted = convert_to_unit(delta, unit)
        pluralized = pluralize(converted, unit)
        return "{} {} ago".format(converted, pluralized)

Aquí ha creado 3 funciones en un nivel inferior de abstracción que son mucho más coherentes y más fáciles de probar de forma aislada. Si dejara fuera el período de tiempo que pretendía, sobresaldría como un pulgar dolorido en las funciones auxiliares más simples. Además, al eliminar la duplicación, reduce la posibilidad de error. En realidad, tendría que agregar código para implementar su caso roto.

Otros casos de prueba más sutiles también vienen a la mente al mirar una forma refactorizada como esta. Por ejemplo, ¿qué debería best_unithacer si deltaes negativo?

En otras palabras, refactorizar no es solo para hacerlo bonito. Facilita a los humanos detectar errores que el compilador no puede.

Karl Bielefeldt
fuente
12
El siguiente paso es internacionalizar, y pluralizesolo trabajar para un subconjunto de palabras en inglés será una responsabilidad.
Deduplicador
@Deduplicator seguro, pero a continuación, dependiendo de qué idiomas / culturas que oriente su campaña, es posible salirse con la única modificación de pluralizeusar numy unitconstruir una clave de algún tipo para tirar de una cadena de formato de algún archivo de la tabla / recurso. O puede que necesite una reescritura completa de la lógica, porque necesita diferentes unidades ;-)
Hulk
44
Aún existe un problema con esta refactorización, que es que "ayer" no tiene mucho sentido en las primeras horas de la mañana (poco después de las 12:01 AM). En términos amigables con los humanos, algo que sucedió a las 11:59 PM no cambia repentinamente de "hoy" a "ayer" cuando el reloj pasa de medianoche. En cambio, cambia de "hace 1 minuto" a "hace 2 minutos". "Hoy" es demasiado grosero en términos de algo que sucedió hace unos minutos, y "ayer" está plagado de problemas para los noctámbulos.
David Hammen
@DavidHammen Este es un problema de usabilidad y depende de cuán preciso sea necesario. Cuando quiera saber al menos hasta la hora, no creo que "ayer" sea bueno. "Hace 24 horas" es mucho más claro y es una expresión humana comúnmente utilizada para enfatizar la cantidad de horas. Las computadoras que intentan ser "amigables con los humanos" casi siempre se equivocan y generalizan en exceso a "ayer", lo cual es demasiado vago. Pero para saber esto, deberá entrevistar a los usuarios para ver qué piensan. Para algunas cosas, realmente desea la fecha y hora exactas, por lo que "ayer" siempre está mal.
Brandin
149

El desarrollo impulsado por pruebas no ayudó.

Parece que ayudó, es solo que no tuvo una prueba para el escenario "hace un día". Presumiblemente, agregó una prueba después de encontrar este caso; esto sigue siendo TDD, ya que cuando se encuentran errores, se escribe una prueba unitaria para detectar el error y luego se corrige.

Si olvida escribir una prueba de comportamiento, TDD no tiene nada que lo ayude; olvida escribir la prueba y, por lo tanto, no escribe la implementación.

esoterik
fuente
2
Se podría señalar que si el desarrollador no hubiera usado tdd, también habría sido mucho más probable que se perdieran otros casos.
Caleb
75
Y, además de eso, ¿piensa en cuánto tiempo se ahorró cuando arreglamos el error? Al tener las pruebas existentes en su lugar, supieron instantáneamente que su cambio no rompió el comportamiento existente. Y eran libres de agregar los nuevos casos de prueba y refactorizar sin tener que ejecutar pruebas manuales extensas después.
Caleb
15
TDD es tan bueno como las pruebas escritas.
Mindwin
Otra observación: agregar la prueba para este caso mejorará el diseño al obligarnos a datetime.utcnow()eliminarlo de la función y, en su lugar, pasarlo nowcomo un argumento (reproducible).
Toby Speight
114

un evento que ocurriera hace veintiséis horas sería hace un día

Las pruebas no ayudarán mucho si un problema está mal definido. Evidentemente, está mezclando días calendario con días calculados en horas. Si se atiene a los días calendario, entonces a la 1 a.m., hace 26 horas no es ayer. Y si se atiene a las horas, hace 26 horas se redondea a 1 día, independientemente de la hora.

Kevin Krumwiede
fuente
45
Este es un gran punto para hacer. La falta de un requisito no significa necesariamente que su proceso de implementación haya fallado. Simplemente significa que el requisito no estaba bien definido. (O simplemente cometió un error humano, que sucederá de vez en cuando)
Caleb
Esta es la respuesta que quería hacer. Definiría la especificación como "si el evento fuera este día calendario, presente el delta en horas. De lo contrario, use las fechas solo para determinar el delta" Las horas de prueba solo son útiles dentro de un día, si más allá de eso, su resolución debe ser días.
Baldrickk
1
Me gusta esta respuesta porque señala el problema real: los puntos en el tiempo y las fechas son dos cantidades diferentes. Están relacionados, pero cuando comienzas a compararlos, las cosas van al sur muy rápido. En programación, la lógica de fecha y hora es una de las cosas más difíciles de corregir. Realmente no me gusta que muchas implementaciones de fecha básicamente almacenen la fecha como un punto 0:00 en el tiempo. Hace mucha confusión.
Pieter B
38

No puedes TDD es excelente para protegerlo de posibles problemas que conoce. No ayuda si te encuentras con problemas que nunca has considerado. Su mejor opción es que alguien más pruebe el sistema, pueden encontrar los casos límite que nunca consideró.

Lectura relacionada: ¿Es posible alcanzar el estado de error absoluto cero para el software a gran escala?

Ian Jacobs
fuente
2
Tener pruebas escritas por alguien que no sea el desarrollador siempre es una buena idea, significa que ambas partes deben pasar por alto la misma condición de entrada para que el error entre en producción.
Michael Kay
35

Hay dos enfoques que normalmente adopto que creo que pueden ayudar.

Primero, busco los casos extremos. Estos son lugares donde cambia el comportamiento. En su caso, el comportamiento cambia en varios puntos a lo largo de la secuencia de días enteros positivos. Hay un caso límite en cero, en uno, en siete, etc. Luego escribiría casos de prueba en y alrededor de los casos límite. Tendría casos de prueba a -1 días, 0 días, 1 hora, 23 horas, 24 horas, 25 horas, 6 días, 7 días, 8 días, etc.

La segunda cosa que buscaría es patrones de comportamiento. En su lógica durante semanas, tiene un manejo especial durante una semana. Probablemente tenga una lógica similar en cada uno de sus otros intervalos que no se muestran. Sin embargo, esta lógica no está presente durante días. Lo miraría con sospecha hasta que pudiera explicar de manera verificable por qué ese caso es diferente, o agregaré la lógica.

cbojar
fuente
9
Esta es una parte realmente importante de TDD que a menudo se pasa por alto y rara vez he visto hablar de él en artículos y guías; es realmente importante probar casos límite y condiciones límite, ya que encuentro que es la fuente del 90% de los errores. - errores, desbordamientos y desbordamientos, último día del mes, último mes del año, años bisiestos, etc.
GoatInTheMachine
2
@GoatInTheMachine - y el 90% de esos errores del 90% están alrededor de las transiciones del horario de verano ... Jajaja
Caleb
1
Primero puede dividir las entradas posibles en clases de equivalencia y luego determinar los casos límite en los bordes de las clases. Por supuesto, ese es un esfuerzo que puede ser mayor que el esfuerzo de desarrollo; si eso vale la pena depende de lo importante que sea entregar software lo más libre de errores posible, cuál es la fecha límite y cuánto dinero y paciencia tiene.
Peter - Restablece a Mónica el
2
Esta es la respuesta correcta. Muchas reglas de negocio requieren que divida un rango de valores en intervalos en los que se manejan casos de diferentes maneras.
abuzittin gillifirca 16/0718
14

No puede detectar errores lógicos que están presentes en sus requisitos con TDD. Pero aún así, TDD ayuda. Encontraste el error, después de todo, y agregaste un caso de prueba. Pero fundamentalmente, TDD solo garantiza que el código se ajuste a su modelo mental. Si su modelo mental es defectuoso, los casos de prueba no los detectarán.

Pero tenga en cuenta que, mientras soluciona el error, los casos de prueba que ya se había asegurado de que no se rompiera ningún comportamiento funcional existente. Eso es bastante importante, es fácil corregir un error pero introducir otro.

Para encontrar esos errores de antemano, generalmente intenta usar casos de prueba basados ​​en la clase de equivalencia. utilizando ese principio, elegiría un caso de cada clase de equivalencia, y luego todos los casos extremos.

Elegiría una fecha de hoy, ayer, hace unos días, exactamente hace una semana y varias semanas como ejemplos de cada clase de equivalencia. Al realizar las pruebas de fechas, también se aseguraría de que sus pruebas no usaran la fecha del sistema, sino que usara una fecha predeterminada para la comparación. Esto también destacaría algunos casos extremos: se aseguraría de ejecutar sus pruebas en algún momento arbitrario del día, lo haría directamente después de la medianoche, directamente antes de la medianoche e incluso directamente a la medianoche. Esto significa que para cada prueba, habría cuatro veces la base contra la cual se prueba.

Luego, agregaría sistemáticamente casos extremos a todas las demás clases. Tienes la prueba para hoy. Por lo tanto, agregue un tiempo justo antes y después de que el comportamiento cambie. Lo mismo para ayer. Lo mismo hace una semana, etc.

Lo más probable es que al enumerar todos los casos límite de una manera sistemática y al escribir casos de prueba para ellos, descubra que su especificación carece de algún detalle y la agregue. Tenga en cuenta que el manejo de las fechas es algo que las personas a menudo se equivocan, porque a menudo se olvidan de escribir sus pruebas para que puedan ejecutarse en diferentes momentos.

Tenga en cuenta, sin embargo, que la mayor parte de lo que he escrito tiene poco que ver con TDD. Se trata de escribir clases de equivalencia y asegurarse de que sus propias especificaciones estén lo suficientemente detalladas sobre ellas. Ese es el proceso con el cual minimizas los errores lógicos. TDD solo se asegura de que su código se ajuste a su modelo mental.

Proponer casos de prueba es difícil . Las pruebas basadas en la clase de equivalencia no son el final de todo, y en algunos casos pueden aumentar significativamente el número de casos de prueba. En el mundo real, agregar todas esas pruebas a menudo no es económicamente viable (aunque en teoría, debería hacerse).

Poligoma
fuente
12

La única forma en que puedo pensar es en agregar muchas afirmaciones para los casos que creo que nunca sucederían (como creía que hace un día es necesariamente ayer), y luego recorrer cada segundo durante los últimos diez años, buscando cualquier violación de afirmación, que parece demasiado compleja.

Por qué no? ¡Esto suena como una muy buena idea!

Agregar contratos (aserciones) al código es una forma bastante sólida de mejorar su corrección. Generalmente los agregamos como precondiciones en la entrada de funciones y postcondiciones en el retorno de funciones. Por ejemplo, podríamos agregar una condición posterior de que todos los valores devueltos tienen la forma "A [unit] ago" o "[number] [unit] s ago". Cuando se realiza de manera disciplinada, esto lleva al diseño por contrato , y es una de las formas más comunes de escribir código de alta seguridad.

Críticamente, los contratos no están destinados a ser probados; son las mismas especificaciones de su código que sus pruebas. Sin embargo, puede realizar la prueba a través de los contratos: llame al código en su prueba y, si ninguno de los contratos genera errores, la prueba pasa. Recorrer cada segundo de los últimos diez años es un poco demasiado. Pero podemos aprovechar otro estilo de prueba llamado prueba basada en propiedades .

En PBT en lugar de probar salidas específicas del código, prueba que la salida obedece a alguna propiedad. Por ejemplo, una propiedad de una reverse()función es que para cualquier lista l, reverse(reverse(l)) = l. La ventaja de escribir pruebas como esta es que puede hacer que el motor PBT genere unos cientos de listas arbitrarias (y algunas patológicas) y verifique que todas tengan esta propiedad. Si alguno no lo hace , el motor "encoge" el caso de falla para encontrar una lista mínima que rompa su código. Parece que estás escribiendo Python, que tiene la hipótesis como el marco principal de PBT.

Por lo tanto, si desea una buena manera de encontrar casos extremos más complicados en los que no piense, el uso conjunto de contratos y pruebas basadas en la propiedad será de gran ayuda. Esto no reemplaza las pruebas unitarias de escritura, por supuesto, pero lo aumenta, lo cual es realmente lo mejor que podemos hacer como ingenieros.

Hovercouch
fuente
2
Esta es exactamente la solución correcta para este tipo de problema. El conjunto de salidas válidas es fácil de definir (podría dar una expresión regular muy simple, algo así como /(today)|(yesterday)|([2-6] days ago)|...) y luego puede ejecutar el proceso con entradas seleccionadas al azar hasta que encuentre una que no esté en el conjunto de salidas esperadas. Tomar este enfoque habría detectado este error y no requeriría darse cuenta de que el error podría existir de antemano.
Julio
@Jules Consulte también comprobación / prueba de propiedades . Normalmente escribo pruebas de propiedad durante el desarrollo, para cubrir tantos casos imprevistos como sea posible y obligarme a pensar en propiedades generales / invariantes.
Guardo
1
Si realiza tantas repeticiones en las pruebas, tomará mucho tiempo, lo que anula uno de los objetivos principales de las pruebas unitarias: ¡ejecutar las pruebas rápidamente !
CJ Dennis
5

Este es un ejemplo donde habría sido útil agregar un poco de modularidad. Si un segmento de código propenso a errores se usa varias veces, es una buena práctica incluirlo en una función si es posible.

def time_ago(delta, unit):
    delta_str = _number_to_text(delta) + " " + unit;
    if delta == 1:
        return delta_str + " ago"
    else:
        return delta_str = "s ago"

now = datetime.datetime.utcnow()
today = now.date()
if event_date.date() == today:
    return "Today"

yesterday = today - datetime.timedelta(1)
if event_date.date() == yesterday:
    return "Yesterday"

delta = (now - event_date).days

if delta < 7:
    return time_ago(delta, "day")

if delta < 30:
    weeks = math.floor(delta / 7)
    return time_ago(weeks, "week")

if delta < 365:
    months = math.floor(delta / 31)
    return time_ago(months, "month")
Antonio Pérez
fuente
5

El desarrollo impulsado por pruebas no ayudó.

TDD funciona mejor como técnica si la persona que escribe las pruebas es contradictoria. Esto es difícil si no está programando en pares, por lo que otra forma de pensar en esto es:

  • No escriba pruebas para confirmar que la función bajo prueba funciona como la hizo. Escribe pruebas que lo rompan deliberadamente.

Este es un arte diferente, que se aplica a la escritura de código correcto con o sin TDD, y tal vez tan complejo (si no más) que escribir código en realidad. Es algo que necesita practicar, y es algo para lo que no hay una respuesta única, fácil y simple.

La técnica central para escribir software robusto es también la técnica central para comprender cómo escribir pruebas efectivas:

Comprenda las condiciones previas para una función: los estados válidos (es decir, qué suposiciones está haciendo sobre el estado de la clase de la cual la función es un método) y los rangos de parámetros de entrada válidos: cada tipo de datos tiene un rango de valores posibles, un subconjunto de los cuales será manejado por su función.

Si simplemente no hace más que probar explícitamente estas suposiciones en la entrada de funciones, y asegurarse de que se registra o se arroja una violación y / o los errores de la función se eliminan sin más manejo, puede saber rápidamente si su software falla en la producción, hágalo robusto y tolerante a errores, y desarrolle sus habilidades de redacción de pruebas adversas.


NÓTESE BIEN. Existe toda una literatura sobre condiciones previas y posteriores, invariantes, etc., junto con bibliotecas que pueden aplicarlas utilizando atributos. Personalmente, no soy fanático de ir tan formal, pero vale la pena investigarlo.

Chris Becke
fuente
1

Este es uno de los hechos más importantes sobre el desarrollo de software: es absolutamente imposible escribir código libre de errores.

TDD no le ahorrará la introducción de errores correspondientes a casos de prueba en los que no pensó. Tampoco te ahorrará escribir una prueba incorrecta sin darte cuenta, luego escribir código incorrecto que pase la prueba de error. Y todas las demás técnicas de desarrollo de software creadas tienen agujeros similares. Como desarrolladores, somos humanos imperfectos. Al final del día, no hay forma de escribir código 100% libre de errores. Nunca ha sucedido y nunca sucederá.

Esto no quiere decir que debas perder la esperanza. Si bien es imposible escribir código completamente perfecto, es muy posible escribir código que tiene tan pocos errores que aparecen en casos tan raros que el software es extremadamente práctico de usar. El software que no muestra un comportamiento defectuoso en la práctica es muy posible de escribir.

Pero escribirlo requiere que aceptemos el hecho de que produciremos software con errores. Casi todas las prácticas modernas de desarrollo de software se basan en algún nivel para evitar que aparezcan errores en primer lugar o para protegernos de las consecuencias de los errores que inevitablemente producimos:

  • La recopilación de requisitos exhaustivos nos permite saber cómo se ve un comportamiento incorrecto en nuestro código.
  • Escribir código limpio y cuidadosamente diseñado hace que sea más fácil evitar la introducción de errores en primer lugar y más fácil de corregir cuando los identificamos.
  • Escribir pruebas nos permite producir un registro de lo que creemos que serían muchos de los peores errores posibles en nuestro software y demostrar que evitamos al menos esos errores. TDD produce esas pruebas antes del código, BDD deriva esas pruebas de los requisitos y las pruebas unitarias antiguas producen pruebas después de que se escribe el código, pero todas evitan las peores regresiones en el futuro.
  • Las revisiones por pares significan que cada vez que se cambia el código, al menos dos pares de ojos han visto el código, lo que disminuye la frecuencia con la que los errores se escapan al maestro.
  • El uso de un rastreador de errores o un rastreador de historias de usuarios que trata los errores como historias de usuarios significa que cuando aparecen errores, se les hace un seguimiento y, en última instancia, se tratan, no se olvidan y se dejan interponer constantemente en las formas de los usuarios.
  • El uso de un servidor provisional significa que, antes de un lanzamiento importante, cualquier error de show-stopper tiene la oportunidad de aparecer y solucionarse.
  • El uso del control de versiones significa que, en el peor de los casos, donde se envía un código con errores importantes a los clientes, puede realizar una reversión de emergencia y obtener un producto confiable en manos de sus clientes mientras soluciona las cosas.

La solución definitiva al problema que has identificado no es luchar contra el hecho de que no puedes garantizar que escribirás un código libre de errores, sino aceptarlo. Adopte las mejores prácticas de la industria en todas las áreas de su proceso de desarrollo, y entregará constantemente código a sus usuarios que, aunque no es perfecto, es lo suficientemente robusto para el trabajo.

Kevin
fuente
1

Simplemente no había pensado en este caso antes y, por lo tanto, no tenía un caso de prueba para ello.

Esto sucede todo el tiempo y es normal. Siempre es una compensación cuánto esfuerzo pones en crear todos los casos de prueba posibles. Puede pasar un tiempo infinito para considerar todos los casos de prueba.

Para un piloto automático de avión, pasaría mucho más tiempo que para una herramienta simple.

A menudo es útil pensar en los rangos válidos de las variables de entrada y probar estos límites.

Además, si el probador es una persona diferente al desarrollador, a menudo se encuentran casos más significativos.

Simon
fuente
1

(y creyendo que tiene que ver con zonas horarias, a pesar del uso uniforme de UTC en el código)

Ese es otro error lógico en su código para el que aún no tiene una prueba de unidad :): su método devolverá resultados incorrectos para los usuarios en zonas horarias que no sean UTC. Debe convertir tanto "ahora" como la fecha del evento a la zona horaria local del usuario antes de calcular.

Ejemplo: en Australia, un evento ocurre a las 9 am hora local. A las 11 am se mostrará como "ayer" porque la fecha UTC ha cambiado.

Sergey
fuente
0
  • Deje que alguien más escriba las pruebas. De esta manera, alguien que no esté familiarizado con su implementación podría verificar situaciones raras en las que no haya pensado.

  • Si es posible, inyecte casos de prueba como colecciones. Esto hace que agregar otra prueba sea tan fácil como agregar otra línea como yield return new TestCase(...). Esto puede ir en la dirección de las pruebas exploratorias , automatizando la creación de casos de prueba: "Veamos qué devuelve el código durante todos los segundos de hace una semana".

nulo
fuente
0

Parece estar bajo la idea errónea de que si pasan todas sus pruebas, no tiene errores. En realidad, si todas sus pruebas pasan, todo el comportamiento conocido es correcto. Aún no sabe si el comportamiento desconocido es correcto o no.

Con suerte, está utilizando cobertura de código con su TDD. Agregue una nueva prueba para el comportamiento inesperado. Luego puede ejecutar solo la prueba del comportamiento inesperado para ver qué camino toma realmente el código. Una vez que conozca el comportamiento actual, puede hacer un cambio para corregirlo, y cuando todas las pruebas pasen nuevamente, sabrá que lo ha hecho correctamente.

Esto todavía no significa que su código esté libre de errores, solo que es mejor que antes, y una vez más, ¡todo el comportamiento conocido es correcto!

Usar TDD correctamente no significa que escribirá código libre de errores, significa que escribirá menos errores. Tu dices:

Los requisitos eran relativamente claros.

¿Significa esto que el comportamiento de más de un día pero no ayer se especificó en los requisitos? Si no cumplió un requisito por escrito, es su culpa. Si se dio cuenta de que los requisitos estaban incompletos mientras lo codificaba, ¡bien por usted! Si todos los que trabajaron en los requisitos perdieron ese caso, no eres peor que los demás. Todos cometen errores, y cuanto más sutiles son, más fáciles son de pasar por alto. La gran conclusión aquí es que TDD no previene todos los errores.

CJ Dennis
fuente
0

Es muy fácil cometer un error lógico incluso en un código fuente tan simple.

Si. El desarrollo impulsado por pruebas no cambia eso. Todavía puede crear errores en el código real y también en el código de prueba.

El desarrollo impulsado por pruebas no ayudó.

¡Oh, pero lo hizo! En primer lugar, cuando notó el error, ya tenía el marco de prueba completo en su lugar, y solo tuvo que corregir el error en la prueba (y el código real). En segundo lugar, no sabe cuántos errores más habría tenido si no hubiera hecho TDD al principio.

También preocupante es que no puedo ver cómo se pueden evitar estos errores.

No puedes Ni siquiera la NASA ha encontrado una manera de evitar errores; nosotros, los humanos menores, ciertamente tampoco lo hacemos.

Aparte de pensar más antes de escribir el código,

Eso es una falacia. Uno de los mayores beneficios de TDD es que puede codificar con menos pensamiento, porque todas esas pruebas al menos captan bastante bien las regresiones. También, incluso, o especialmente con TDD, se no se espera que produzca código libre de errores en el primer lugar (o su velocidad de desarrollo simplemente se detendría).

la única forma en que puedo pensar es en agregar muchas afirmaciones para los casos que creo que nunca sucederían (como creía que hace un día es necesariamente ayer), y luego recorrer cada segundo durante los últimos diez años, buscando cualquier violación de afirmación, que parece demasiado compleja.

Esto claramente entraría en conflicto con el principio de solo codificar lo que realmente necesita en este momento. Pensaste que necesitabas esos casos, y así fue. Era un código no crítico; como dijiste que no hubo daños, excepto que te lo preguntaste durante 30 minutos.

Para el código de misión crítica, en realidad podría hacer lo que dijo, pero no para su código estándar diario.

¿Cómo podría evitar crear este error en primer lugar?

Usted no Confía en sus pruebas para encontrar la mayoría de las regresiones; mantienes el ciclo rojo-verde-refactor, escribes pruebas antes / durante la codificación real y (¡importante!) implementas la cantidad mínima necesaria para hacer el cambio rojo-verde (ni más, ni menos). Esto terminará con una excelente cobertura de prueba, al menos positiva.

Cuando, si no, encuentra un error, escribe una prueba para reproducir ese error y lo repara con la menor cantidad de trabajo para hacer que dicha prueba pase de rojo a verde.

AnoE
fuente
-2

Acabas de descubrir que no importa cuánto lo intentes, nunca podrás detectar todos los posibles errores en tu código.

Entonces, lo que esto significa es que incluso intentar atrapar todos los errores es un ejercicio inútil, por lo que solo debe usar técnicas como TDD como una forma de escribir un código mejor, un código que tenga menos errores, no 0 errores.

Eso, a su vez, significa que debe pasar menos tiempo usando estas técnicas y gastar ese tiempo ahorrado trabajando en formas alternativas de encontrar los errores que se escapan de la red de desarrollo.

alternativas como pruebas de integración, o un equipo de prueba, pruebas de sistema, y ​​registrar y analizar esos registros.

Si no puede atrapar todos los errores, entonces debe tener una estrategia para mitigar los efectos de los errores que se le escapan. Si tiene que hacer esto de todos modos, poner más esfuerzo en esto tiene más sentido que intentar (en vano) detenerlos en primer lugar.

Después de todo, no tiene sentido gastar una fortuna en el tiempo escribiendo pruebas y el primer día que entrega su producto a un cliente se cae, particularmente si no tiene idea de cómo encontrar y resolver ese error. La resolución de errores post-mortem y post-entrega es muy importante y necesita más atención de la que la mayoría de las personas dedica a escribir pruebas unitarias. Guarde las pruebas unitarias para los bits complicados y no intente la perfección por adelantado.

gbjbaanb
fuente
Esto es extremadamente derrotado. That in turn means you should spend less time using these techniques- ¿Pero acabas de decir que ayudará con menos errores?
JᴀʏMᴇᴇ
@ JᴀʏMᴇᴇ más una actitud pragmática de qué técnica te hace ganar más dinero. Conozco a personas que están orgullosas de que pasan 10 veces escribiendo pruebas de lo que hicieron en su código, y todavía tienen errores . sobre las técnicas de prueba es esencial. Y las pruebas de integración tienen que usarse de todos modos, así que pon más esfuerzo en ellas que en las pruebas unitarias.
gbjbaanb