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 delta
es 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?
fuente
Respuestas:
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):
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_unit
hacer sidelta
es negativo?En otras palabras, refactorizar no es solo para hacerlo bonito. Facilita a los humanos detectar errores que el compilador no puede.
fuente
pluralize
solo trabajar para un subconjunto de palabras en inglés será una responsabilidad.pluralize
usarnum
yunit
construir 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 ;-)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.
fuente
datetime.utcnow()
eliminarlo de la función y, en su lugar, pasarlonow
como un argumento (reproducible).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.
fuente
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?
fuente
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.
fuente
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).
fuente
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 listal
,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.
fuente
/(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.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.
fuente
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:
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.
fuente
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 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.
fuente
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.
fuente
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.
fuente
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".fuente
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:
¿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.
fuente
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.
¡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.
No puedes Ni siquiera la NASA ha encontrado una manera de evitar errores; nosotros, los humanos menores, ciertamente tampoco lo hacemos.
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).
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.
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.
fuente
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.
fuente
That in turn means you should spend less time using these techniques
- ¿Pero acabas de decir que ayudará con menos errores?