Prueba de unidad de código funcional estáticamente tipado

15

Quería preguntarles a las personas, en cuyo caso tiene sentido probar un código funcional estáticamente tipado, como está escrito en haskell, scala, ocaml, nemerle, f # o haXe (el último es lo que realmente me interesa, pero quería aprovechar el conocimiento de las comunidades más grandes).

Pregunto esto porque, según tengo entendido:

  • Un aspecto de las pruebas unitarias es tener las especificaciones en forma ejecutable. Sin embargo, cuando se emplea un estilo declarativo, que asigna directamente las especificaciones formalizadas a la semántica del lenguaje, ¿es incluso posible expresar las especificaciones en forma ejecutable de manera separada, lo que agrega valor?

  • El aspecto más obvio de las pruebas unitarias es rastrear errores que no pueden revelarse a través del análisis estático. Dado que el código funcional de tipo seguro es una buena herramienta para codificar extremadamente cerca de lo que entiende su analizador estático, parece que puede cambiar mucha seguridad hacia el análisis estático. Sin embargo, un simple error como usar en xlugar de y(ambos siendo coordenadas) en su código no puede ser cubierto. OTOH, tal error también podría surgir al escribir el código de prueba, por lo que no estoy seguro de si vale la pena el esfuerzo.

  • Las pruebas unitarias introducen redundancia, lo que significa que cuando los requisitos cambian, el código que los implementa y las pruebas que cubren este código deben cambiarse. Esta sobrecarga, por supuesto, es casi constante, por lo que se podría argumentar que realmente no importa. De hecho, en lenguajes como Ruby realmente no se compara con los beneficios, pero dado que la programación funcional tipada estáticamente cubre muchas de las pruebas de unidades terrestres, parece que se trata de una sobrecarga constante que simplemente se puede reducir sin penalización.

De esto deduciría que las pruebas unitarias son algo obsoletas en este estilo de programación. Por supuesto, tal afirmación solo puede conducir a guerras religiosas, así que permítanme resumir esto en una simple pregunta:

Cuando utiliza un estilo de programación de este tipo, ¿en qué medida utiliza pruebas unitarias y por qué (qué calidad espera obtener para su código)? O al revés: ¿tiene criterios por los cuales puede calificar una unidad de código funcional tipado estáticamente según lo cubierto por el analizador estático y, por lo tanto, sin necesidad de cobertura de prueba unitaria?

back2dos
fuente
44
Por cierto, si no has probado QuickCheck , definitivamente deberías.
Jon Purdy
scalacheck.org es el equivalente de Scala
V-Lamp

Respuestas:

8

Un aspecto de las pruebas unitarias es tener las especificaciones en forma ejecutable. Sin embargo, cuando se emplea un estilo declarativo, que asigna directamente las especificaciones formalizadas a la semántica del lenguaje, ¿es incluso posible expresar las especificaciones en forma ejecutable de manera separada, lo que agrega valor?

Si tiene especificaciones que se pueden asignar directamente a las declaraciones de funciones, está bien. Pero típicamente esos son dos niveles completamente diferentes de abstracciones. Las pruebas unitarias están destinadas a probar piezas de código individuales, escritas como pruebas de recuadro blanco por el mismo desarrollador que está trabajando en la función. Las especificaciones normalmente se ven como "cuando ingreso este valor aquí y presiono este botón, esto y aquello debería suceder". Típicamente, tal especificación lleva a desarrollar y probar más de una función.

Sin embargo, un error simple como usar x en lugar de y (ambas son coordenadas) en su código no puede ser cubierto. Sin embargo, tal error también podría surgir al escribir el código de prueba, por lo que no estoy seguro de si vale la pena el esfuerzo.

Su idea errónea es que las pruebas unitarias son en realidad para encontrar errores en su código de primera mano; eso no es cierto, al menos, solo es parcialmente cierto. Están hechos para evitar que introduzcas errores más tarde cuando tu código evoluciona. Entonces, cuando primero probó su función y su prueba de unidad funciona (con "x" e "y" en su lugar), y luego, al refactorizar, usa x en lugar de y, entonces la prueba de unidad se lo mostrará.

Las pruebas unitarias introducen redundancia, lo que significa que cuando los requisitos cambian, el código que los implementa y las pruebas que cubren este código deben cambiarse. Esta sobrecarga, por supuesto, es casi constante, por lo que se podría argumentar que realmente no importa. De hecho, en lenguajes como Ruby realmente no se compara con los beneficios, pero dado que la programación funcional tipada estáticamente cubre muchas de las pruebas de la unidad terrestre, parece que es una sobrecarga constante que simplemente se puede reducir sin penalización.

En ingeniería, la mayoría de los sistemas de seguridad dependen de la redundancia. Por ejemplo, dos descansos en un automóvil, un paracaídas redundante para un buceador, etc. La misma idea se aplica a las pruebas unitarias. Por supuesto, tener más código para cambiar cuando cambian los requisitos puede ser una desventaja. Por lo tanto, especialmente en las pruebas unitarias, es importante mantenerlas SECAS (siga el principio "No se repita"). En un idioma de tipo estático, es posible que deba escribir menos pruebas unitarias que en un idioma de tipo débil. Es posible que no sean necesarias pruebas "formales", lo cual es bueno, ya que le da más tiempo para trabajar en las pruebas unitarias importantes que prueban cosas importantes. Y no piense solo porque tiene tipos estáticos, no necesita pruebas unitarias, todavía hay mucho espacio para introducir errores durante la refactorización.

Doc Brown
fuente
5

Un aspecto de las pruebas unitarias es tener las especificaciones en forma ejecutable. Sin embargo, cuando se emplea un estilo declarativo, que asigna directamente las especificaciones formalizadas a la semántica del lenguaje, ¿es incluso posible expresar las especificaciones en forma ejecutable de manera separada, lo que agrega valor?

Es muy poco probable que pueda expresar completamente sus especificaciones como restricciones de tipo.

Cuando usa ese estilo de programación, ¿en qué medida usa pruebas unitarias y por qué (qué calidad espera obtener para su código)?

De hecho, uno de los principales beneficios de este estilo es que las funciones puras son más fáciles de probar por unidad: no es necesario configurar el estado externo o verificarlo después de la ejecución.

A menudo, la especificación (o parte de ella) de una función se puede expresar como propiedades que relacionan el valor devuelto con los argumentos. En este caso, el uso de QuickCheck (para Haskell) o ScalaCheck (para Scala) puede permitirle anotar estas propiedades como expresiones del lenguaje y verificar que tengan entradas aleatorias.

Alexey Romanov
fuente
1
Un poco más de detalles sobre QuickCheck: la idea básica es que escriba "propiedades" (invariantes en su código) y especifique cómo generar una entrada potencial. Quickcheck crea una tonelada de entradas aleatorias y se asegura de que su invariante se mantenga en todos los casos. Es más exhaustivo que las pruebas unitarias.
Tikhon Jelvis
1

Puede pensar en las pruebas unitarias como un ejemplo de cómo usar el código, junto con una descripción de por qué es valioso.

Aquí hay un ejemplo, en el que el tío Bob tuvo la amabilidad de acompañarme en el "Juego de la vida" de John Conway . Creo que es un excelente ejercicio para este tipo de cosas. La mayoría de las pruebas son de sistema completo, probando todo el juego, pero esa primera prueba solo una función: la que calcula los vecinos alrededor de una celda. Puede ver que todas las pruebas están escritas en forma declarativa, con el comportamiento que estamos buscando claramente explicado.

También es posible burlarse de las funciones utilizadas en las funciones; ya sea pasándolos a la función (el equivalente a la inyección de dependencia) o con marcos como el Midje de Brian Marick .

Lunivore
fuente
0

Sí, las pruebas unitarias ya tienen sentido con el código funcional tipado estáticamente. Un simple ejemplo:

prop_encode a = (decode . encode $ a) == a

Puedes forzar prop_encodecon tipo estático.

Zhen
fuente