¿Cómo se detectarían los errores de tipo al crear simulacros en un lenguaje dinámico?

10

El problema ocurre al hacer TDD. Después de un par de pasadas de prueba, los tipos de retorno de alguna clase / módulo cambian. En un lenguaje de programación de tipo estático, si se utilizó un objeto simulado anterior en las pruebas de otra clase y no se modificó para reflejar el cambio de tipo, se producirán errores de compilación.

Sin embargo, para lenguajes dinámicos, el cambio en los tipos de retorno podría no detectarse y las pruebas de la otra clase aún pasarán. Claro que podría haber pruebas de integración que deberían fallar más adelante, pero las pruebas unitarias pasarían erróneamente. ¿Hay alguna forma de evitar esto?

Actualización con una muestra trivial (en un lenguaje inventado) ...

Versión 1:

Calc = {
    doMultiply(x, y) {return x * y}
}
//.... more code ....

// On some faraway remote code on a different file
Rect = {
    computeArea(l, w) {return Calc.doMultipy(x*y)}
}

// test for Rect
testComputeArea() { 
    Calc = new Mock()
    Calc.expect(doMultiply, 2, 30) // where 2 is the arity
    assertEqual(30, computeArea)
}

Ahora, en la versión 2:

// I change the return types. I also update the tests for Calc
Calc = {
    doMultiply(x, y) {return {result: (x * y), success:true}}
}

... Rect arrojará una excepción en el tiempo de ejecución, pero la prueba aún tendrá éxito.

jvliwanag
fuente
1
Lo que hasta ahora parecen fallar las respuestas es que la pregunta no se trata de las pruebas que involucran el cambio class X, sino de las pruebas de las class Ycuales depende Xy, por lo tanto, se prueba con un contrato diferente al que se ejecuta en la producción.
Bart van Ingen Schenau
Acabo de hacer esta pregunta sobre SO , yo mismo, con respecto a la inyección de dependencia. Consulte la razón 1: una clase dependiente se puede cambiar en tiempo de ejecución (piense en las pruebas) . Ambos tenemos la misma mentalidad pero nos faltan explicaciones geniales.
Scott Coates
Estoy releyendo tu pregunta, pero estoy un poco confundido con la interpretación. ¿Puede dar un ejemplo?
Scott Coates

Respuestas:

2

Hasta cierto punto, esto es solo parte del costo de hacer negocios con lenguajes dinámicos. Obtienes mucha flexibilidad, también conocida como "cuerda suficiente para ahorcarte". Ten cuidado con eso.

Para mí, el problema sugiere utilizar técnicas de refactorización diferentes a las que usaría en un lenguaje de tipo estático. En un lenguaje estático, reemplaza los tipos de retorno en parte para que pueda "apoyarse en el compilador" para encontrar qué lugares pueden romperse. Es seguro hacerlo, y probablemente sea más seguro que modificar el tipo de retorno en su lugar.

En un lenguaje dinámico, no puede hacer eso, por lo que es mucho más seguro modificar el tipo de retorno en lugar de reemplazarlo. Posiblemente, lo modifique agregando su nueva clase como miembro, para las clases que lo necesitan.

tallseth
fuente
2

Si su código cambia y sus pruebas aún pasan, entonces hay algo mal con sus pruebas (es decir, le falta una afirmación), o el código en realidad no cambió.

¿Qué quiero decir con eso? Bueno, sus pruebas describen los contratos entre partes de su código. Si el contrato es "el valor de retorno debe ser iterable", entonces cambiar el valor de retorno de una matriz a una lista no es en realidad un cambio en el contrato y, por lo tanto, no necesariamente desencadenará una falla de prueba.

Para evitar aserciones faltantes, puede usar herramientas como el análisis de cobertura de código (no le dirá qué partes de su código se prueban, pero le dirá qué partes definitivamente no se prueban), la complejidad ciclomática y la complejidad de NPath (que le dará un límite inferior en el número de afirmaciones requiere para lograr la plena cobertura de código C1 y C2) y mutación probadores (que inyecte mutaciones en el código como girar truea false, los números negativos en positivos, objetos en nulletc., y comprobar si ese hace que las pruebas fallen).

Jörg W Mittag
fuente
+1 O algo mal con las pruebas o el código en realidad no cambió. Me tomó un tiempo acostumbrarme después de años de C ++ / Java. El contrato entre partes en lenguajes dinámicos de código no debe ser QUÉ se devuelve, sino qué contiene esa cosa que se devuelve y qué puede hacer.
MrFox
@suslik: En realidad, no se trata de lenguajes estáticos o dinámicos, sino de abstracción de datos utilizando tipos de datos abstractos versus abstracción de datos orientada a objetos. La capacidad de un objeto para simular otro objeto siempre que se comporte indistinguible (incluso si son de tipos completamente diferentes e instancias de clases completamente diferentes) es fundamental para toda la idea de OO. O dicho de otra manera: si dos objetos se comportan igual, son del mismo tipo, independientemente de lo que digan sus clases. Dicho de otra manera: las clases no son tipos. Desafortunadamente, Java y C # se equivocan.
Jörg W Mittag el
Suponiendo que la unidad simulada está 100% cubierta y no falta ninguna afirmación: ¿qué tal un cambio más sutil como "debería devolver nulo para foo" a "debería devolver una cadena vacía para foo". Si alguien cambia esa unidad y su prueba, algunas unidades consumidoras pueden romperse y, debido al simulacro, esto es transparente. (Y no: al momento de escribir o usar el módulo simulado, por "contrato", no es necesario manejar cadenas vacías como valor de retorno porque se supone que devuelve cadenas no vacías o solo nulas;)). (Solo las pruebas de integración adecuadas de ambos módulos en interacción podrían detectar esto).
try-catch-finally