Testing

Utilidades básicas para testing unitario que incorpora la librería estándar de Python.

docstrings

Son cadenas especiales que explican el uso básico de una base o función. Están pensadas para el usuario de la función, no para el desarrolador de la misma. Se colocan bajo la "cabecera" de la función o clase.


In [16]:
def factorial(x):
    """
    Return the factorial of x.
    """
    return 1 if not x else x * factorial(x - 1)

In [17]:
help(factorial)


Help on function factorial in module __main__:

factorial(x)
    Return the factorial of x.

Esta cadena se almacena como un atributo de la función (con nombre __doc__):


In [18]:
print(factorial.__doc__)


    Return the factorial of x.
    

Sí, las funciones también son objetos.


In [19]:
print(type(factorial))
print("---")
print(dir(factorial))


<type 'function'>
---
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']

Los docstrings pueden contener ejemplos de uso, lo que ayuda a entender la finalidad de la función. Además sirven para probar la función (doctest).


In [20]:
%%file tmp/factorial.py
def factorial(x):
    """
    Return the factorial of x.
    
    >>> factorial(0)
    1
    >>> factorial(6)
    720
    """
    return 1 if not x else x * factorial(x - 1)


Overwriting tmp/factorial.py

Veamos cómo ejecutar esta prueba (desde un programa Python):


In [21]:
import doctest
doctest.testmod()


Out[21]:
TestResults(failed=0, attempted=0)

También se puede ejecutar desde un terminal:


In [22]:
!python -m doctest -v tmp/factorial.py


Trying:
    factorial(0)
Expecting:
    1
ok
Trying:
    factorial(6)
Expecting:
    720
ok
1 items had no tests:
    factorial
1 items passed all tests:
   2 tests in factorial.factorial
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

El módulo unittest

Es un módulo para crear test unitarios. Es muy similar a los framework de testing habituales en otros lenguajes, como JUnit (Java). Con unittest las pruebas unitarias consisten en crear clases que heredan de la clase unittest.TestCase. Cada método de la clase que empiece con test_ es una prueba indipendiente. Recuerda que las pruebas unitarias deben cumplir FIRST para serlo.

  • Fast
  • Independent (o Isolated)
  • Repeatable
  • Self-validating
  • Timely

Probemos (testemos) la función factorial:


In [23]:
%%file test/factorial_tests.py

import sys
import unittest
sys.path.append("./tmp")

from factorial import factorial

class FactorialTests(unittest.TestCase):
    def test_factorial_0(self):
        self.assertEquals(factorial(0), 1)
        
    def test_factorial_6(self):
        self.assertEquals(factorial(6), 720)


Overwriting test/factorial_tests.py

Se pueden ejecutar con el módulo estándar:


In [24]:
!python -m unittest -v test.factorial_tests


test_factorial_0 (test.factorial_tests.FactorialTests) ... ok
test_factorial_6 (test.factorial_tests.FactorialTests) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

O con otros runners, como python-nose:


In [25]:
!nosetests -v test/factorial_tests.py


test_factorial_0 (test.factorial_tests.FactorialTests) ... ok
test_factorial_6 (test.factorial_tests.FactorialTests) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

OK, FAIL y ERROR


In [26]:
%%file test/sample.py

from unittest import TestCase
import socket

class SampleTest(TestCase):
    def test_ok(self):
        self.assertEquals(1, 1)
        
    def test_fail(self):
        self.assertEquals(1, 0)
        
    def test_error(self):
        1/0


Overwriting test/sample.py

In [27]:
!nosetests -v test/sample.py


test_error (test.sample.SampleTest) ... ERROR
test_fail (test.sample.SampleTest) ... FAIL
test_ok (test.sample.SampleTest) ... ok

======================================================================
ERROR: test_error (test.sample.SampleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/david/repos/python-intro/test/sample.py", line 13, in test_error
    1/0
ZeroDivisionError: integer division or modulo by zero

======================================================================
FAIL: test_fail (test.sample.SampleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/david/repos/python-intro/test/sample.py", line 10, in test_fail
    self.assertEquals(1, 0)
AssertionError: 1 != 0

----------------------------------------------------------------------
Ran 3 tests in 0.002s

FAILED (errors=1, failures=1)

Como vemos en esta salida, una prueba puede producir 3 tres resultados diferentes:

  • OK: no ocurrió ningún error y todas las aserciones se cumplieron.
  • FAIL: Alguna aserción falló.
  • ERROR: Se produjo un error en la propia prueba (probablemente una excepción no capturada).

[E] Escribe un testcase para la clase BankAccount y prueba los métodos deposit(), transfer_to() y la versión modificada de withdraw() que lanza excepción.


In [29]:
from unittest import TestCase
from bank import BankAccount

class BankAccountTest(TestCase):
    def test_deposit(self):
        self.account = BankAccount()
        self.account.deposit(300)
        self.assertEquals(self.account.balance, 300)

    def test_withdraw(self):
        self.account = BankAccount()
        self.account.deposit(300)
        self.account.withdraw(100)
        self.assertEquals(self.account.balance, 200)

    def test_withdraw_raises_NotEnoughBalance(self):
        self.account = BankAccount()
        self.account.deposit(300)
        with self.assertRaises(NotEnoughBalance):
            self.account.withdraw(400)

setUp y tearDown

Es habitual que varias pruebas necesiten condiciones de comienzo similares, por ejemplo, una instancia de la clase que se está probando. En ese caso se puede utilizar un método especial que debe llamarse setUp, que será ejecutado antes de cada prueba. Existe otro método similar (llamado tearDown) que se ejecuta después de cada prueba y que se puede utilizar para labores de cierre y limpieza, o quizá comprobación de alguna postcondición común.


In [30]:
from unittest import TestCase
from bank import BankAccount

class BankAccountTest(TestCase):
    def setUp(self):
        self.account = BankAccount()
        self.account.deposit(300)

    def test_deposit(self):
        self.assertEquals(self.account.balance, 300)

    def test_withdraw(self):
        self.account.withdraw(100)
        self.assertEquals(self.account.balance, 200)

    def test_withdraw_raises_NotEnoughBalance(self):
        with self.assertRaises(NotEnoughBalance):
            self.account.withdraw(400)

    def tearDown(self):
        assert self.account.balance >= 0

TDD

[ToDo]


In [31]:
from IPython.display import SVG
SVG('figures/tdd.svg')


Out[31]:
image/svg+xml fail ok refactor