Utilidades básicas para testing unitario que incorpora la librería estándar de Python.
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)
Esta cadena se almacena como un atributo de la función (con nombre __doc__):
In [18]:
print(factorial.__doc__)
Sí, las funciones también son objetos.
In [19]:
print(type(factorial))
print("---")
print(dir(factorial))
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)
Veamos cómo ejecutar esta prueba (desde un programa Python):
In [21]:
import doctest
doctest.testmod()
Out[21]:
También se puede ejecutar desde un terminal:
In [22]:
!python -m doctest -v tmp/factorial.py
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.
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)
Se pueden ejecutar con el módulo estándar:
In [24]:
!python -m unittest -v test.factorial_tests
O con otros runners, como python-nose:
In [25]:
!nosetests -v test/factorial_tests.py
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
In [27]:
!nosetests -v test/sample.py
Como vemos en esta salida, una prueba puede producir 3 tres resultados diferentes:
[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)
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
In [31]:
from IPython.display import SVG
SVG('figures/tdd.svg')
Out[31]: