Pruebas Unitarias para Haskell

De Wikihaskell
Saltar a: navegación, buscar
Pruebas unitarias para Haskell
Trabajo sobre pruebas unitarias en Haskell.
Lenguaje Haskell
Biblioteca test-framework
test-framework-hunit
test-frameworkquichek
Autores Eugenio Juárez Clavín
Francisco José Rubio Sánchez
Mario Rivas Sánchez

HUnit -- Haskell Unit Testing es un framework, desarrollado por Dean Herington, que facilita y automatiza la realización de pruebas de código para el lenguaje funcional Haskell. Es similar al JUnit tool [1] para Java.


Contenido

Concepto de Prueba Unitaria

Consisten en comprobaciones (manuales o automatizadas) que se realizan para verificar que el código correspondiente a un módulo concreto de un sistema software funciona de acuerdo con los requisitos del sistema.

La fase de pruebas es una de las más costosas del ciclo de vida software. Generan grandes pérdidas (en tiempo y dinero), ya que los errores son muchos más difíciles y costosos de detectar después de la generación del código.

Para solventar este problema se deberían realizar pruebas a todos los “artefactos” que generemos en el ciclo de vida de una aplicación: especificaciones de requisitos, casos de uso, diagramas, el código, bases de datos, ...

No obstante también debemos tener en cuenta que es prácticamente imposible probar todo: por tiempo y coste.

Resumidamente, para poder entregar Software de cierta garantía de calidad, debemos comprobar que el software :

  • hace lo que debe
  • no hace lo que no debe


Normas relacionadas

ISO 9126 (2005)[2] es un estándar internacional para la evaluación del Software. Clasifica la calidad del software en un conjunto estructurado de características: funcionabilidad, fiabilidad, usabilidad, eficiencia, mantenibilidad, portabilidad.


IEEE 829 (1983)[3] es un estándar para documentar pruebas del Software. Detalla 8 etapas en el proceso de documentación de las pruebas:

  • Plan de prueba
  • Especificación de los requerimientos para el diseño de los casos de prueba
  • Casos de prueba
  • Descripción del procedimiento de prueba
  • Descripción del item a probar
  • Registro de la prueba
  • Reporte de incidentes de prueba
  • Resumen de pruebas


ISO/IEC 12207 (1995)[4] es el estándar para los procesos de ciclo de vida del software de la organización ISO. No define, como vemos, un proceso de Pruebas como tal:

  • proceso de Validación: ¿el producto es correcto?
  • proceso de Verificación: ¿se está construyendo el producto correctamente?

Las pruebas en los Modelos de Desarrollo del Software

M 1.JPG

Imagen sacada de: "Mantenimiento Avanzado de Sistemas de Información. Pruebas del Software" de Dr. Macario Polo Usaola


M 2.JPG

Imagen sacada de: "Mantenimiento Avanzado de Sistemas de Información. Pruebas del Software" de Dr. Macario Polo Usaola


Imagen sacada de: "Mantenimiento Avanzado de Sistemas de Información. Pruebas del Software" de Dr. Macario Polo Usaola M 3.JPG

Tipos de pruebas

  • Pruebas Unitarias: básicamente comprueban el código
  • Pruebas de Sistema: demuestran que los sistemas cumplen con los requerimientos
  • Pruebas de Integración: comprueban la iteración entre componentes
  • Pruebas de aceptación: simulan un ambiente de operación para comprobar que se cumplen las especificaciones.
  • Pruebas de Regresión: se realizan tras incluir modificaciones, para comprobar que no se han producido situaciones indeseadas


Enfoques para la realización de las pruebas

  • Estructural o caja blanca: realizan un seguimiento del código fuente.
  • Funcional o caja negra: realiza la prueba de clases en aplicaciones Orientadas a Objetos
  • Aleatorio: emplean modelos estadísticos para representar las entradas que usaremos para crear los casos de prueba


"... El testing puede probar la presencia de errores, pero no la ausencia de ellos."

(E.W.Dijkstra)

Caja blanca negra.JPG

Test Framework

Un framework paralelo para combinar pruebas hechas usando QuickCheck y HUnit.

Descripción

Test-framework es un framework que permite realizar pruebas de software con valores generados mediante QuickCheck y casos de prueba HUnit. Además, la incorporación de ambos dotan al framework de los siguientes rasgos:

  • Ejecutar test en paralelo con un informe de resultados en orden determinístico.
  • Informe de progresos para propiedades individuales realizados por QuickCheck.
  • Filtrar las pruebas para ser ejecutadas usando patrones específicos desde la línea de comandos.
  • Resultados de las pruebas presentados jerárquicamente y con realce de colores.
  • Informe de estadísticas de los test realizados(número de test ejecutados, fallados, etc).
  • Posibilidad de personalizar nuestros propios test.
  • A través de Quickcheck podemos reproducir la secuencia de valores usados como semilla, entonces nos permite reproducir el entorno de los casos de error si es necesario.

Instalación

El primer paso a dar para poder realizar la instalación de las bibliotecas necesarias es asegurarnos de tener instalado Cabal. Una vez hemos realizado este paso simplemente se debería ejecutar las siguientes instrucciones:

$ cabal install test-framework
$ cabal install test-framework-quickcheck
$ cabal install test-framework-quickcheck2
$ cabal install test-framework-hunit

HUnit

  • HUnit es un framework para pruebas de unidad realizado para Haskell, e inspirado por la herramienta de Java: JUnit.
  • Fue creado en 2002 por Dean Herington estudiante graduado en la Universidad de Carolina del Norte, en el departamento de Ciencias de la computación.
  • Con HUnit, como con JUnit, podemos fácilmente crear test, nombrarlos, agruparlos dentro de suites, y ejecutarlos con el marco de trabajo que valida los resultados automáticamente.

Tipo de dato Test

El tipo de dato Test es el tipo fundamental del modulo HUnit. A partir del tipo Test es posible definir los test que se van a realizar. El tipo Test esta definido de la siguiente manera:

               data Test = TestCase Assertion
             	           | TestList [Test]
                           | TestLabel String Test

Alguna de los constructores de Test son:

  • TestCase representa una unidad de ejecución de test. Cada uno de estos casos de test son independientes entres si, por lo que el fallo de uno es independiente del fallo de cualquier otro. Un TestCase consiste en uno o varios Assertion. Por ejemplo el TestCase (TestCase (return ())) es un test que nunca falla, el test (TestCase (assertEqual "for x," 3 x)) es un test que chequea que el valor de x sea igual a 3.
  • TestList es una lista de Test del tipo TestCase. Este tipo debe de incluir el conjunto de TestCase que se desean ejecutar.
  • TestLabel nos permite asignar un nombre a un TestCase dentro de un TestList

Esto se podrá ver con mas claridad a continuación con un ejemplo.

Tipo Assertion

En la base del funcionamiento de HUnit se encuentran las "aserciones" o assertions. Estas se combinan para crear los test case (caso de prueba) que a su vez se agrupan para formar tests (suite de casos de prueba).

Las aserciones se emplean para asociar a parte de código Haskell un posible valor, en el caso de no cumplirse se genera un error que mostrará el pertinente mensaje cuando se ejecute la batería de pruebas.

El tipo base es la mónada Assertion que es usada en la función assertFailure:

   type Assertion = IO ()
   assertFailure :: String -> Assertion
   assertFailure msg = ioError (userError ("HUnit:" ++ msg))

Donde msg corresponde al testo a mostrar cuando se ejecute la prueba y se produzca algún fallo. Para indicar cuando entendemos que se ha producido un fallo se emplean las siguientes funciones que condicionalmente llaman a asserFailure:

   assertBool :: String -> Bool -> Assertion
   assertBool msg b = unless b (assertFailure msg)
   assertString :: String -> Assertion
   assertString s = unless (null s) (assertFailure s)
   assertEqual :: (Eq a, Show a) => String -> a -> a -> Assertion
   assertEqual preface expected actual =
     unless (actual == expected) (assertFailure msg)
    where msg = (if null preface then "" else preface ++ "\n") ++
                "expected: " ++ show expected ++ "\n but got: " ++ show actual

Desde el punto de vista de un usuario Hunit nos quedaremos con:

  • assertBool: mostrará un mensaje de fallo si falla la condición dada
  • assertString: mostrará un mensaje de fallo si la expresión que le pasamos devuelve algo (no nulo).
  • assertEqual: mostrará un mensaje de fallo si la ejecución de la función dada no coincide con los valores proporcionados. Es la más frecuente. Tiene tres argumentos: mensaje a mostrar, valores esperados y código a probar.

Ejemplo:

   ...
   assertEqual "Para (foo 3)," (1,2) (foo 3))
   ...
   > runTestTT tests
   ### Failure in: 0:test1
   Para(foo 3)
   expected: (1,2)
    but got: (1,3)
   ....

Tipo Count

Todos los controladores de prueba comparten un modelo de ejecución común, que se diferencian sólo en cómo muestran los resultados de ejecución de las pruebas. La ejecución de una prueba ( un valor de tipo Test ) es ejecutada en el orden primero en profundidad,de izquierda a derecha. Durante la ejecución de las pruebas, cuatro counts de casos de prueba se tienen en cuenta:

        data Counts = Counts { cases, tried, errors, failures :: Int }
                      deriving (Eq, Show, Read)
  • cases es el número de casos de prueba incluidos en el test. Este número es una propiedad estática de un test y permanece inalterado durante la ejecución de las pruebas.
  • tried es el número de casos de prueba que han sido ejecutados hasta ahora durante la ejecución de los test.
  • errors es el número de casos de prueba cuya ejecución finalizó con una excepción inesperada, siendo parada dicha ejecución. Errors indica problemas con los casos de test, en oposición con el código bajo el que se está escrito el test.
  • failures es el número de casos de prueba cuya ejecución indica problemas con el código con el que está escrito el test.

Ejecución de Test

HUnit está estructurado para soportar múltiples controladores de test. La sección anterior (Tipo de dato Count) describe las características comunes de todos los controladores de test. Se indica que todos los controladores tienen un modelo de ejecución común, diferenciándose sólo en los resultados de ejecución del test (recogidos a través del tipo de dato count). Existe un segundo aspecto en la ejecución de los test, y es el controlador basado en texto' incluido en HUnit:

    runTestText :: PutText st -> Test -> IO (Counts, st)

runTestText se recoge en un esquema que realiza un informe, el cual se recibe como primer argumento. Durante la ejecución de los test, dado como segundo argumento (Test), el controlador crea un String por cada evento del informe y lo procesa conforme al esquema. Cuando la ejecución del test se completa, el controlador devuelve el count final de acuerdo con el estado final del test. Los tres tipos de informe son:

  • Un informe inicial(Start report), resultado de aplicar la función showCounts a los counts siguientes antes de que el caso de prueba haya comenzado.
  • Un informe de errores(Error report) de la forma : " Error in: path\nmessage", donde path es el directorio del caso de test que contiene el error, mostrándose con showPath, y message es un mensaje descriptivo del error.
  • Un informe de fallos (Failure report) de la forma: Failure in: path\nmessage, donde path es el directorio del caso de test que contiene el error, mostrándose con showPath, y message es un mensaje descriptivo del fallo.

HUnit posee un elemento para el uso de controlador de test basado en texto:

   runTestTT :: Test -> IO Counts

runTestTT invoca a la función runTestText, que a partir del informe, devuelve el counts final de la ejecución del test

Ejemplos

Primer ejemplo:

     	module Main where
       import HUnit
       -- Funciones que se van a provar
       foo :: Int -> (Int, Int)
       foo x = (1, x)
       partA :: Int -> IO (Int, Int)
       partA v = return (v+2, v+3)
       partB :: Int -> IO Bool
       partB v = return (v > 5)
       --Creación de los Test
       test1 :: Test
       test1 = TestCase (assertEqual "Prueba de (foo 3)," (1,2) (foo 3))
       test2 :: Test
       test2 = TestCase (do (x,y) <- partA 3
                            assertEqual "Prueba del primer elemento de partA," 5 x
                            b <- partB y
                            assertBool ("(partB " ++ show y ++ ") fallo") b)
       tests :: Test
       tests = TestList [TestLabel "test1" test1, TestLabel "test2" test2]
       --Ejecución del test
       main :: IO Counts
       main = runTestTT tests

Segundo Ejemplo:

       module Main where
       import HUnit
       -- Lista en la que se almacenara todas las posibles permutaciones del verctor [1..5]
       permutaciones = perms [1..5]
       -- Funciones auxiliares para la generación de las permutaciones
       intercala :: a -> [a] -> [[a]]
       intercala x [] = [[x]]
       intercala x ls@(y:ys) = (x:ls) : map (y :) (intercala x ys)
       perms :: [a] -> [[a]]
       perms [] = [[]]
       perms (x:xs) = concat ( map ( intercala x ) (perms xs))
       -- Función a probar
       ordRapida::Ord a => [a]->[a]
       ordRapida [] = []
       ordRapida (x:r) = ordRapida menores ++ [x] ++ ordRapida mayores
                         where
                            menores = [y | y <- r, y < x]
                            mayores = [y | y <- r, y >= x]
      -- Creacion de los test
      test1 :: Test
      test1 = TestCase (assertEqual "Ordenación rápida" [1..5] (ordRapida (permutaciones !! 34)))
      test2 = TestCase (assertEqual "Ordenación rápida" [1..5] (ordRapida (permutaciones !! 13)))
      test3 = TestCase (assertEqual "Ordenación rápida" [1..5] (ordRapida (permutaciones !! 67)))
      test4 = TestCase (assertEqual "Ordenación rápida" [1..5] (ordRapida [1..5]))
      test5 = TestCase (assertEqual "Ordenación rápida" [1..5] (ordRapida [5..1]))
      tests :: Test
      tests = TestList [TestLabel "test1" test1,
                        TestLabel "test2" test2, 
                        TestLabel "test3" test3,
                        TestLabel "test4" test4,
                        TestLabel "test5" test5]
      --Ejecución de los tests
      main :: IO Counts
      main = do runTestTT tests

Documentación

Herramientas personales