Leí sobre este problema: el error de programación le cuesta a Citigroup $ 7 millones después de transacciones legítimas confundidas con datos de prueba durante 15 años .
Cuando se introdujo el sistema a mediados de la década de 1990, el código del programa filtró las transacciones que recibieron códigos de sucursal de tres dígitos del 089 al 100 y utilizó esos prefijos para fines de prueba.
Pero en 1998, la compañía comenzó a usar códigos de sucursal alfanuméricos a medida que expandía su negocio. Entre ellos estaban los códigos 10B, 10C, etc., que el sistema trató como dentro del rango excluido, por lo que sus transacciones se eliminaron de cualquier informe enviado a la SEC.
(Creo que esto ilustra que usar un indicador de datos no explícito es ... subóptimo. Hubiera sido mucho mejor poblar y usar una Branch.IsLive
propiedad semánticamente explícita ).
Aparte de eso, mi primera reacción fue "Las pruebas unitarias habrían ayudado aquí" ... pero ¿lo harían?
Recientemente leí ¿Por qué la mayoría de las pruebas unitarias son desperdicio con interés? Entonces , mi pregunta es: ¿cómo serían las pruebas unitarias que habrían fallado en la introducción de códigos de ramificación alfanuméricos?
fuente
Respuestas:
¿Realmente está preguntando, "las pruebas unitarias habrían ayudado aquí?", O está preguntando, "¿podría algún tipo de prueba haber ayudado aquí?".
La forma más obvia de prueba que habría ayudado es una afirmación previa en el código mismo, de que un identificador de rama consta solo de dígitos (suponiendo que este es el supuesto en el que se basa el codificador para escribir el código).
Esto podría haber fallado en algún tipo de prueba de integración, y tan pronto como se introducen los nuevos identificadores de ramificación alfanuméricos, la afirmación explota. Pero esa no es una prueba unitaria.
Alternativamente, podría haber una prueba de integración del procedimiento que genera el informe SEC. Esta prueba garantiza que cada identificador de sucursal real informe sus transacciones (y, por lo tanto, requiere una entrada del mundo real, una lista de todos los identificadores de sucursal en uso). Así que tampoco es una prueba unitaria.
No puedo ver ninguna definición o documentación de las interfaces involucradas, pero puede ser que las pruebas unitarias no puedan haber detectado el error porque la unidad no estaba defectuosa . Si se permite que la unidad asuma que los identificadores de sucursal consisten solo en dígitos, y los desarrolladores nunca tomaron una decisión sobre qué debería hacer el código en caso de que no lo hiciera, entonces no deberíanescriba una prueba unitaria para imponer un comportamiento particular en el caso de identificadores que no sean dígitos porque la prueba rechazaría una implementación hipotética válida de la unidad que manejó correctamente los identificadores de rama alfanuméricos, y generalmente no desea escribir una prueba unitaria que impida futuras implementaciones y extensiones. O tal vez un documento escrito hace 40 años implícitamente definido (a través de algún rango lexicográfico en EBCDIC sin procesar, en lugar de una regla de clasificación más amigable para los humanos) que 10B es un identificador de prueba porque de hecho cae entre 089 y 100. Pero entonces Hace 15 años, alguien decidió usarlo como un identificador real, por lo que la "falla" no reside en la unidad que implementa correctamente la definición original: se encuentra en el proceso que no se dio cuenta de que 10B se define como un identificador de prueba y, por lo tanto, no debe asignarse a una rama. Lo mismo sucedería en ASCII si definiera 089-100 como un rango de prueba y luego introdujera un identificador 10 $ o 1.0. Simplemente sucede que en EBCDIC los dígitos vienen después de las letras.
Una prueba unitaria (o posiblemente una prueba funcional) que posiblementepodría haber salvado el día, es una prueba de la unidad que genera o valida nuevos identificadores de rama. Esa prueba afirmaría que los identificadores deben contener solo dígitos y se escribirían para permitir que los usuarios de los identificadores de rama asuman lo mismo. O tal vez hay una unidad en algún lugar que importa identificadores de rama reales pero nunca ve los de prueba, y eso podría probarse de forma unitaria para garantizar que rechaza todos los identificadores de prueba (si los identificadores son solo tres caracteres, podemos enumerarlos todos y comparar el comportamiento de el validador al del filtro de prueba para asegurarse de que coinciden, que se ocupa de la objeción habitual a las pruebas puntuales). Luego, cuando alguien cambió las reglas, la prueba unitaria habría fallado ya que contradice el comportamiento recién requerido.
Dado que la prueba estuvo allí por una buena razón, el punto en el que debe eliminarlo debido a los requisitos comerciales cambiados se convierte en una oportunidad para que alguien reciba el trabajo ", encuentre cada lugar en el código que se base en el comportamiento que queremos cambio". Por supuesto, esto es difícil y, por lo tanto, poco confiable, por lo que de ninguna manera garantizaría salvar el día. Pero si captura sus suposiciones en las pruebas de las unidades de las que está asumiendo propiedades, entonces se ha dado una oportunidad y, por lo tanto, el esfuerzo no se desperdicia por completo .
Por supuesto, estoy de acuerdo en que si la unidad no se hubiera definido en primer lugar con una entrada de "forma divertida", entonces no habría nada que probar. Las divisiones de espacio de nombres complicadas pueden ser difíciles de probar correctamente porque la dificultad no radica en implementar su definición divertida, sino en asegurarse de que todos entiendan y respeten su definición divertida. Esa no es una propiedad local de una unidad de código. Además, cambiar algún tipo de datos de "una cadena de dígitos" a "una cadena de caracteres alfanuméricos" es similar a hacer que un programa basado en ASCII maneje Unicode: no será simple si su código está fuertemente acoplado a la definición original, y cuando el tipo de datos es fundamental para lo que hace el programa, entonces a menudo está fuertemente acoplado.
Si las pruebas unitarias a veces fallan (mientras está refactorizando, por ejemplo), y al hacerlo le brindan información útil (su cambio es incorrecto, por ejemplo), entonces el esfuerzo no se desperdició. Lo que no hacen es probar si su sistema funciona. Entonces, si está escribiendo pruebas unitarias en lugar de tener pruebas funcionales y de integración, entonces puede estar usando su tiempo de manera subóptima.
fuente
Las pruebas unitarias podrían haber detectado que los códigos de rama 10B y 10C se clasificaron incorrectamente como "ramas de prueba", pero encuentro improbable que las pruebas para esa clasificación de rama hayan sido lo suficientemente extensas como para detectar ese error.
Por otro lado, las comprobaciones puntuales de los informes generados podrían haber revelado que 10B y 10C ramificados no aparecían en los informes mucho antes de los 15 años en que ahora se permitía que el error permaneciera presente.
Finalmente, esta es una buena ilustración de por qué es una mala idea mezclar datos de prueba con los datos de producción reales en una base de datos. Si hubieran utilizado una base de datos separada que contiene los datos de las pruebas, no habría sido necesario filtrar eso de los informes oficiales y habría sido imposible filtrar demasiado.
fuente
El software tuvo que manejar ciertas reglas comerciales. Si hubiera pruebas unitarias, las pruebas unitarias habrían comprobado que el software manejó las reglas comerciales correctamente.
Las reglas del negocio cambiaron.
Aparentemente, nadie se dio cuenta de que las reglas comerciales habían cambiado, y nadie cambió el software para aplicar las nuevas reglas comerciales. Si hubiera habido pruebas unitarias, esas pruebas unitarias tendrían que cambiarse, pero nadie lo habría hecho porque nadie se dio cuenta de que las reglas comerciales habían cambiado.
Entonces no, las pruebas unitarias no habrían captado eso.
La excepción sería si las pruebas unitarias y el software hubieran sido creados por equipos independientes, y el equipo que realiza las pruebas unitarias cambiara las pruebas para aplicar las nuevas reglas comerciales. Entonces las pruebas unitarias habrían fallado, lo que con suerte habría resultado en un cambio del software.
Por supuesto, en el mismo caso si solo se cambiara el software y no las pruebas unitarias, entonces las pruebas unitarias también fallarían. Cuando una prueba unitaria falla, no significa que el software esté mal, significa que el software o la prueba unitaria (a veces ambos) están mal.
fuente
No. Este es uno de los grandes problemas con las pruebas unitarias: te inducen a una falsa sensación de seguridad.
Si pasan todas sus pruebas, no significa que su sistema esté funcionando correctamente; significa que todas tus pruebas están pasando . Significa que las partes de tu diseño en las que pensaste y escribiste pruebas conscientemente funcionan como creías conscientemente que lo harían, lo que en realidad no es un gran problema: eso fue lo que realmente estabas prestando mucha atención a, así que es muy probable que lo hayas hecho bien de todos modos! Pero no hace nada para detectar casos en los que nunca pensó, como este, porque nunca pensó en escribir una prueba para ellos. (Y si lo hubiera hecho, habría sabido que eso significaba que los cambios de código eran necesarios, y los habría cambiado).
fuente
No, no necesariamente
El requisito original era usar códigos de ramificación numéricos, por lo que se habría producido una prueba unitaria para un componente que aceptara varios códigos y rechazara cualquiera como 10B. El sistema habría pasado como funcionando (que era).
Entonces, el requisito habría cambiado y los códigos actualizados, pero esto habría significado que el código de prueba de la unidad que proporcionó los datos incorrectos (que ahora son buenos datos) tendría que modificarse.
Ahora suponemos que las personas que administran el sistema sabrían que este es el caso y cambiarían la prueba de la unidad para manejar los nuevos códigos ... pero si supieran que está ocurriendo, también habrían sabido cambiar el código que manejó estos códigos de todos modos ... y no hicieron eso. Una prueba unitaria que originalmente rechazó el código 10B hubiera dicho felizmente "todo está bien aquí" cuando se ejecuta, si no supiera actualizar esa prueba.
Las pruebas unitarias son buenas para el desarrollo original pero no para las pruebas del sistema, especialmente no 15 años después de que los requisitos se hayan olvidado por mucho tiempo.
Lo que necesitan en este tipo de situación es una prueba de integración de extremo a extremo. Uno donde puede pasar los datos que espera que funcionen y ver si lo hace. Alguien habría notado que sus nuevos datos de entrada no producían un informe y luego investigarían más a fondo.
fuente
Las pruebas de tipo (el proceso de probar invariantes utilizando datos válidos generados aleatoriamente, como lo ejemplifica la biblioteca de pruebas de Haskell QuickCheck y varios puertos / alternativas inspirados en otros idiomas) pueden haber detectado este problema, las pruebas unitarias casi seguramente no habrían hecho .
Esto se debe a que cuando se actualizaron las reglas para la validez de los códigos de sucursal, es poco probable que alguien hubiera pensado probar esos rangos específicos para asegurarse de que funcionaran correctamente.
Sin embargo, si la prueba de tipo hubiera estado en uso, alguien debería, en el momento en que se implementó el sistema original, haber escrito un par de propiedades, una para verificar que los códigos específicos para las ramas de prueba fueron tratados como datos de prueba y otro para verificar que no haya otros códigos fueron ... cuando se actualizó la definición del tipo de datos para el código de bifurcación (que habría sido necesario para permitir probar que alguno de los cambios para el código de bifurcación de dígito a numérico funcionó), esta prueba habría comenzado a probar valores en el nuevo rango y muy probablemente habría identificado la falla.
Por supuesto, QuickCheck se desarrolló por primera vez en 1999, por lo que ya era demasiado tarde para detectar este problema.
fuente
Realmente dudo que las pruebas unitarias hagan una diferencia en este problema. Parece una de esas situaciones de visión de túnel porque la funcionalidad se cambió para admitir nuevos códigos de sucursal, pero esto no se llevó a cabo en todas las áreas del sistema.
Utilizamos pruebas unitarias para diseñar una clase. Volver a ejecutar una prueba unitaria solo es necesario si el diseño ha cambiado. Si una unidad en particular no cambia, las pruebas de unidad sin cambios arrojarán los mismos resultados que antes. Las pruebas unitarias no le mostrarán los impactos de los cambios en otras unidades (si lo hacen, no está escribiendo pruebas unitarias).
Solo puede detectar razonablemente este problema a través de:
No tener suficientes pruebas de extremo a extremo es más preocupante. No puede confiar en las pruebas unitarias como su ÚNICA o la prueba PRINCIPAL para los cambios del sistema. Parece que solo requiere que alguien ejecute un informe sobre los formatos de código de sucursal recientemente admitidos.
fuente
Una afirmación integrada en el tiempo de ejecución podría haber ayudado; por ejemplo:
bool isTestOnly(string branchCode) { ... }
Ver también:
fuente
La conclusión de esto es Fail Fast .
No tenemos el código, ni tenemos muchos ejemplos de prefijos que son o no prefijos de ramificación de prueba de acuerdo con el código. Todo lo que tenemos es esto:
El hecho de que el código permita números y cadenas es más que un poco extraño. Por supuesto, 10B y 10C pueden considerarse números hexadecimales, pero si todos los prefijos se tratan como números hexadecimales, 10B y 10C quedan fuera del rango de prueba y se tratarán como ramas reales.
Esto probablemente significa que el prefijo se almacena como una cadena pero en algunos casos se trata como un número. Aquí está el código más simple que se me ocurre que replica este comportamiento (usando C # con fines ilustrativos):
En inglés, si la cadena es un número y está entre 89 y 100, es una prueba. Si no es un número, es una prueba. De lo contrario, no es una prueba.
Si el código sigue este patrón, ninguna prueba unitaria lo habría detectado en el momento en que se implementó el código. Aquí hay algunos ejemplos de pruebas unitarias:
La prueba unitaria muestra que "10B" debe tratarse como una rama de prueba. El usuario @ gnasher729 anterior dice que las reglas comerciales cambiaron y eso es lo que muestra la última afirmación anterior. En algún momento esa afirmación debería haber cambiado a an
isFalse
, pero eso no sucedió. Las pruebas unitarias se ejecutan en tiempo de desarrollo y construcción, pero luego en ningún momento posterior.¿Cuál es la lección aquí? El código necesita alguna forma de indicar que recibió una entrada inesperada. Aquí hay una forma alternativa de escribir este código que enfatiza que espera que el prefijo sea un número:
Para aquellos que no conocen C #, el valor de retorno indica si el código pudo analizar o no un prefijo de la cadena dada. Si el valor de retorno es verdadero, el código de llamada puede usar la variable isTest out para verificar si el prefijo de rama es un prefijo de prueba. Si el valor de retorno es falso, el código de llamada debe informar que no se espera el prefijo dado y que la variable isTest out no tiene sentido y debe ignorarse.
Si está bien con excepciones, puede hacer esto en su lugar:
Esta alternativa es más sencilla. En este caso, el código de llamada debe detectar la excepción. En cualquier caso, el código debería tener alguna forma de informar al llamante que no esperaba un strPrefix que no se pudiera convertir a un entero. De esta manera, el código falla rápidamente y el banco puede encontrar rápidamente el problema sin la multa vergonzosa de la SEC.
fuente
Tantas respuestas y ni siquiera una cita de Dijkstra:
Por lo tanto, depende. Si el código se probó correctamente, lo más probable es que este error no exista.
fuente
Creo que una prueba unitaria aquí habría asegurado que el problema nunca existiera en primer lugar.
Considera, has escrito la
bool IsTestData(string branchCode)
función.La primera prueba unitaria que escriba debe ser para una cadena nula y vacía. Luego, para cadenas de longitud incorrectas y luego para cadenas no enteras.
Para que todas esas pruebas pasen, deberá agregar la comprobación de parámetros a la función.
Incluso si solo prueba datos `` buenos '' 001 -> 999 sin pensar en la posibilidad de 10A, la verificación de parámetros lo obligará a reescribir la función cuando comience a usar alfanuméricos para evitar las excepciones que arrojará
fuente
IsValidBranchCode
función para realizar esta verificación? ¿Y esta función probablemente habría cambiado sin necesidad de modificar elIsTestData
? Entonces, si solo está probando 'buenos datos', la prueba no habría ayudado. La prueba de caso límite habría tenido que incluir un código de sucursal ahora válido (y no simplemente algunos aún inválidos) para comenzar a fallar.