Contenido

Python: Cómo hacer pruebas 3: nose y hamcrest

Después del artículo Python: Cómo hacer pruebas (2), viene el (3) :D

En esta ocasión temo que el tutorial va a ser un poco light, pero ya lo compensaremos con el próximo. Nos vamos a centrar en dos herramientas para hacer tests. Estas herramientas son nose y Hamcrest .

Ambos son paquetes Debian y sencillos de instalar.

Python

Nose

Nose es una herramienta que nos permitirá ejecutar nuestros tests de una manera sencilla y cómoda.

Para ejecutarlo, usaremos la orden nosetests, seguida del archivo o archivos a probar. No es necesario que los archivos tengan nada especial, salvo que éstos deben definir las pruebas en función a unittest o doctest.

Cuando utilizamos nosetests no son necesarias las líneas:

1
2
if __name__ == "__main__":
    unittest.main()

De la misma manera, cuando utilizamos doctest no es necesario indicar:

1
2
if __name__ == "__main__":
   doctest.testmod()

Aunque en el caso de doctest tendremos que habilitarlo en nosetests mediante la opción –with-doctests.

Veremos ejemplos en el apartado de hamcrest.

Truco

No sé dónde vi un truco para avisar que los tests deben ejecutarse con nosetests. El truco es muy sencillo, y consiste en escribir las siguientes líneas:

1
2
if __name__ == "__main__":
   print 'You must run these tests with nose.'

Fácil y estúpido, pero realmente útil.

Sin embargo, también tenemos la opción de usar nose directamente, utilizando:

1
2
3
import nose
if __name__ == "__main__":
   nose.main()

O bien, si queremos saber si el resultado es válido o no:

1
2
3
import nose
if __name__ == "__main__":
   result = nose.run()

Ventajas

Entre las ventajas de usar nose está que podemos lanzar sólo los tests que fallaron en la última ejecución, seleccionar si queremos las pruebas doctest o unittest, pruebas de covertura (aunque requiere algunos módulos aparte), posibilidad de guardar perfiles para reutilizarlos, …

Hamcrest

Es una herramienta que nos facilita la comprobación de condiciones. No sólo está en Python, sino que también está disponible en Java, C++, Objetive C, PHP y Erlang.

Además, provee una sintaxis más semántica que usando el tradicional XUnit. ¿Qué significa esto? Pues que podremos “leer” el código.

Y como ya he escrito mucho, veamos un ejemplo.

Ejemplo 1: Fibonacci

Ya sabéis, esa secuencia en la que cada número es la suma de los dos anteriores , comenzando por dos 1 seguidos (1, 1, 2, 3, 5, 8, 13, …)

Como son ejemplos sencillos, voy a poner el código de prueba y el código válido en el mismo bloque. Para probarlo, usaremos nose

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import unittest
from hamcrest import *


class testFibonacci (unittest.TestCase):
    def testFirstElementIs1(self):
        assert_that(fibonacci(1), equal_to(1))

    def testSecondElementIs1(self):
        assert_that(fibonacci(2), equal_to(1))

    def testThirdElementIs2(self):
        assert_that(fibonacci(3), equal_to(2))

    def testFourthElementIs3(self):
        assert_that(fibonacci(4), equal_to(3))

    def testFifthElementIs5(self):
        assert_that(fibonacci(5), equal_to(5))

def fibonacci (n):
    a = 1
    b = 1
    for x in xrange(n-2):
        c = a + b
        a = b
        b = c
    return b

Fijaros en un test cualquiera (son todos iguales, a lo mejor deberíamos refactorizar): son bastante verbosos. Comparando:

1
2
assert_that(fibonacci(5), equal_to(5))
assertEquals(5, fibonacci(5))

Vemos que es ligeramente más clara la versión Hamcrest que la habitual.

Ejemplo 2: fibonacci como vector

Voy a implementar el mismo algoritmo, pero ahora lo que quiero es que me devuelva un vector con todos los valores anteriores al dado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import unittest
from hamcrest import *


class testFibonacci (unittest.TestCase):
    def testVectorForValue1(self):
        assert_that(fibonacci(1), only_contains(1))

    def testVectorForValue2(self):
        assert_that(fibonacci(2), only_contains(1, 1))

    def testVectorForValue3(self):
        assert_that(fibonacci(3), only_contains(1, 1, 2))

    def testVectorForValue4(self):
        assert_that(fibonacci(4), only_contains(1, 1, 2, 3))

    def testVectorForValue5(self):
        assert_that(fibonacci(5), only_contains(1, 1, 2, 3, 5))

def fibonacci (n):
    if n == 1:
        return [1]
    result = [1, 1]

    for x in xrange(n-2):
        result.append(result[-1] + result[-2])
    return result

Y comparemos cómo sería el test habitual:

1
2
assert_that(fibonacci(5), only_contains(1, 1, 2, 3, 5))
assertEquals([1, 1, 2, 3, 5], fibonacci(5))

Nuevamente es ligeramente más clara la versión Hamcrest.

La verdad es que como Python es tan sencillo, ¡¡resulta complicadísimo encontrar un ejemplo en el que Hamcrest resulte claramente vencedor!! Con Java es muchísimo más sencillo dar el trofeo a Hamcrest.

Sin embargo, me guardo un as en la manga. Aunque la comprobación no aporta mucho más, veamos un error provocado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ nosetests fibonacci.py
...F
======================================================================
FAIL: testVectorForValue4 (fibonacci.testFibonacci)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "fibonacci.py", line 19, in testVectorForValue4
    assert_that(fibonacci(4), only_contains(1, 1, 2))
AssertionError:
Expected: a sequence containing items matching (<1> or <1> or <2>)
     but: was <[1, 1, 2, 3]>


----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=1)
$

¡¡¡Ahá!!! ¿Véis qué claro ha quedado el error?

Más información

Tenéis toda la información que necesitéis en los sites de nose y Hamcrest.

Para más información, podéis ver el siguiente tutorial de python, donde hablaremos de los dobles de pruebas.

Espero que hayáis encontrado útil este tutorial.