Unit Testing en Python
Introducción
En esta entrada vamos a ver como utilizar el framework de testing
“unittest”, para verificar que el código que escribimos está
libre de errores.
Esta es una introducción muy básica al mundo del testing
mediante python. Al final de la entrada encontraréis más recursos
con los que podréis ampliar vuestro conocimiento sobre Testing.
Vamos a realizar un programa que devuelva “Python” si se le
pasa un número divisible por 3; que devuelva “Diario” si el
número es divisible por 5; y si no es divisible ni por 3 ni por 5
que devuelva el número que se ha pasado como argumento.
Escribir tests para nuestra aplicación hará que la calidad de
nuestro código mejore, ya que debemos enfocarnos en un único test a
la vez, y por tanto evitaremos el tratar de resolver varios problemas
al mismo tiempo, o anticiparnos y tratar de resolver problemas que se puedan plantear en un
futuro. De esta forma nos enfocamos únicamente en conseguir que el
Test que acabamos de escribir pase (sea válido).
Vamos a utilizar las normas que dicta TDD (Test-driven
Development) para escribir y llevar a cabo los tests. Para escribir
un Test debemos seguir los 3 siguientes pasos:
-
Escribir un test que falle (Red)
-
Hacer que el test pase (sea
válido) (Green) -
Refactorizar (Opcional) (Refactor)
ejercicio haciendo uso de Tests.
Creamos el primer Test
Lo primero que debemos hacer es crear
el fichero donde vamos a escribir nuestros tests. El fichero se llamará: test_python_diario_software.py
Y meter el siguiente código:
import unittest import python_diario class TestPythonSoftware(unittest.TestCase): def test_should_return_python_when_number_is_3(self): self.assertEqual('Python', python_diario.get_string(3)) if __name__ == '__main__': unittest.main()
El código consiste de las siguientes
partes:
- Importamos el módulo de unittest, que nos permite realizar
los test de nuestra aplicación. - Lo siguiente es importar el módulo sobre el cual queremos
hacer los tests (python_diario). Es importante resaltar que este módulo/fichero aún no lo
hemos creado, y que lo crearemos posteriormente. - Creamos una clase que contendrá todos nuestros Tests. Si no
tienes claro como funcionan las clases en Python, es conveniente que
te pases por este tutorial introductorio:
https://www.pythondiario.com/2014/10/clases-y-objetos-en-python-programacion.html - Definimos un test. Para ello creamos un método dentro de la
clase y lo nombramos con el prefijo “test_”. Es imprescindible
que el método tenga el prefijo “test_”, ya que si no es así el
test nunca se ejecutará.
Como vemos el nombre del método debe
describir qué es lo que vamos a testear. En este caso, queremos
comprobar que cuando le pasamos el número 3, nuestro programa devuelve la palabra “Python”. - La línea de self.assertEqual... comprueba que la función
“get_string” de nuestro módulo “python_diario” devuelve la
palabra “Python” cuando le pasamos como argumento el número 3. - La línea de if __name__ == '__main__': sencillamente sirve
para que al ejecutar al fichero test_python_diario_software.py desde
la consola, se ejecuten de forma automática todos los Tests
creados.
Ejecutamos el Test
Ya tenemos todo listo para ejecutar el test. Vamos a la consola y
tecleamos:
>> python3 test_python_diario_software.py
Veremos la siguiente salida:
Traceback (most recent call last):
File "test_python_diario_software.py", line 2, in
<module>
import python_diario
ImportError: No module named 'python_diario'
Test, antes de escribir cualquier línea de código en nuestro
programa. Es por ello que nosotros hemos escrito primero el Test.
Creando el módulo/fichero que va a ser testeado
Ahora vamos a escribir el código de nuestro programa. Para ello
vamos a crear el fichero “python_diario.py” en la misma carpeta
donde hemos creado el fichero “test_python_diario_software.py”.
En “python_diario.py” escribimos el siguiente código:
def get_string(number): return 'Python'
Ok, aquí hay que mencionar que lo único que hemos hecho es
devolver la palabra 'Python' cada vez que se llama a la función
get_string. Esta es otra norma de TDD: Debemos escribir el mínimo
código posible para hacer que el Test pase. En este caso hacer un
return de 'Python' es lo mínimo que debemos escribir para hacer
pasar el Test.
Puede resultar raro, pero debemos seguir esta práctica, ya que aporta el beneficio de resolver un "problema" de la forma más rápida, y después ya tendremos tiempo de refactorizar. Esto aplicado a un producto real, hará que si surge algún problema, lo primero será enfocarse en resolver el problema de la forma más rápida, y después ya habrá tiempo de hacer un código más elegante o eficiente.
Comprobando que el Test pasa
Vamos a comprobar que el test pasa:
>> python3 test_python_diario_software.py -v
test_should_return_python_when_number_is_3 (__main__.TestPythonSoftware) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Refactorizar (Opcional)
Perfecto el Test pasa. Por lo que podemos pasar al paso 3:
Refactorizar.
Refactorizar consiste en limpiar el código que hemos escrito de tal forma que sea más legible y semántico sin modificar su comportamiento. En este caso nuestro código es tan simple que no es necesario
refactorizar.
Hay que tener en cuenta que la refactorización únicamente se puede llevar a cabo cuando los Tests están en Verde.
Fin de la primera iteración de Testing
Hemos terminado la primera iteración de Testing:
- Escribir un test que falla. (Red)
- Escribir el mínimo código para hacer que el Test pase. (Green)
- Refactorizar
Iteración 2
Vamos a comenzar con la segunda iteración, por lo que vamos a repetir los 3 pasos básicos del Testing. Así que vamos a crear un nuevo test que falle.
1.- Escribir un Test que falle (Red)
Para ello agregamos el siguiente
método en nuestra clase TestPythonSofware que se encuentra en el
fichero test_python_diario_software.py:
def test_should_return_diario_when_number_is_5(self): self.assertEqual('Diario', python_diario.get_string(5))
El test que acabamos de crear comprueba que si pasamos el número 5, la función get_string debe devolver la cadena: “Diario”.
Ejecutamos el test y comprobamos como falla:
>> python3 test_python_diario_software.py
======================================================================
FAIL: test_should_return_diario_when_number_is_5
(__main__.TestPythonSoftware)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_python_diario_software.py", line 12, in
test_should_return_diario_when_number_is_5
self.assertEqual('Diario', python_diario.get_string(5))
AssertionError: 'Diario' != 'Python'
- Diario
+ Python
Vemos como la salida de la ejecución de los test nos indica que
el test “test_should_return_diario_when_number_is_5”
está fallando, ya que debería devolver “Diario” y está
devolviendo “Python” ('Diario' != 'Python').
2.- Escribir el mínimo código para que el Test pase (Green)
Es hora de hacer que este test pase, para ello vamos a reescribir
por completo la función get_string del fichero python_diario.py:
def get_string(number): return 'Python' if number == 3 else 'Diario'
Ahora comprobamos que los tests pasan:
>> python3 test_python_diario_software.py
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Perfecto. Como vemos, hemos escrito lo justo y necesario para
hacer que nuestro código pase los tests.
3.- Refactorizar
De momento no es necesario refactorizar.
Iteración 3
Ahora vamos a agregar nuevos Tests, de
tal forma que comprobemos más de un número divisible por 3, y más
de un número divisible por 5.
1.- Escribir un Test que falle (Red)
Vallamos uno por uno. Primero agregamos un test para
comprobar que el número 6 devuelve 'Python'. De tal forma que
nuestro Test (test_python_diario_software.py) quedará así:
def test_should_return_python_when_number_is_6(self): self.assertEqual('Python', python_diario.get_string(6))
Ejecutamos los Tests y comprobamos que fallan:
>> python3 test_python_diario_software.py ..F ====================================================================== FAIL: test_should_return_python_when_number_is_6 (__main__.TestPythonSoftware) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_python_diario_software.py", line 12, in test_should_return_python_when_number_is_6 self.assertEqual('Python', python_diario.get_string(6)) AssertionError: 'Python' != 'Diario' - Python + Diario ---------------------------------------------------------------------- Ran 3 tests in 0.001s FAILED (failures=1)
2.- Escribir el mínimo código para que el Test pase (Green)
Vamos a escribir el mínimo código para hacer pasar el Test (python_diario.py):
def get_string(number): return 'Python' if number in [3, 6] else 'Diario'
>> python3
test_python_diario_software.py
..
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
Perfecto, los Tests pasan.
Iteración 4
Ahora vamos a agregar un nuevo Test y vamos a seguir los pasos de
hacer que el Test falle, y escribir el mínimos código posible para
hacer que el Test pase. No pongo la salida de los tests para no hacer
demasiado larga la entrada.
1.- Escribir un Test que falle (Red)
El test que vamos a crear es el
siguiente (test_python_diario_software.py):
def test_should_return_python_when_number_is_9(self): self.assertEqual('Python', python_diario.get_string(9))
2.- Escribir el mínimo código para que el Test pase (Green)
python_diario.py:
def get_string(number): return 'Python' if number in [3, 6, 8] else 'Diario'
3.- Refactorizar
Ahora que todos los Tests están en verde, es momento de refactorizar. Ya que estamos viendo que existen
varios tests para los números divisibles por 3 (3, 6 y 9), es
conveniente adaptar el código de python_diario.py para que todos los números divisibles
por 3 devuelvan 'Python'. Para ello escribiremos la función
get_string de la siguiente manera (python_diario.py):
def get_string(number): return 'Python' if number % 3 == 0 else 'Diario'
Siempre que refactorizamos debemos volver a correr los Tests:
>> python3 test_python_diario_software.py
..
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
A esto que hemos hecho se le llama “triangular”. Cuando hacemos tests, únicamente nos ocupamos del caso actual que estamos testeando, y de
no romper ninguno de los test anteriores, nunca nos preocupamos de
los futuros casos que el programa debe contemplar.
En un momento
dado nos daremos cuenta de un patrón que cuadra perfectamente para
resolver el test actual y los anteriores, es en ese caso, cuando estemos en el
paso de “refactorizar”, escribiremos un algoritmo que sirva para que los tests ya escritos pasen, y que además servirá para futuros casos.
Por
ejemplo, en nuestro caso no tiene sentido escribir más de 3 tests
para los números múltiplos de 3, ya que podríamos estar añadiendo
de forma infinita números múltiplos de 3 ([3,6,9,12,15,...]). En
algún momento debemos “generalizar” nuestra solución, y esto
suele ocurrir cuando hemos escrito 3 tests, de ahí el nombre de
“triangular”.
Iteración 5
1.- Escribir un Test que falle (Red)
Vamos a crear un nuevo test que compruebe que el programa devuelve
el número que le pasamos (test_python_diario_software.py).
def test_should_return_7_when_number_is_7(self): self.assertEqual(7, python_diario.get_string(7))
Corremos el Test y vemos como falla:
>> python3 test_python_diario_software.py F.... ====================================================================== FAIL: test_should_return_7_when_number_is_7 (__main__.TestPythonSoftware) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_python_diario_software.py", line 21, in test_should_return_7_when_number_is_7 self.assertEqual(7, python_diario.get_string(7)) AssertionError: 7 != 'Diario' ---------------------------------------------------------------------- Ran 5 tests in 0.001s FAILED (failures=1)
2.- Escribir el mínimo código para que el Test pase (Green)
Vamos a escribir el mínimo código posible para hacer pasar el
Test (python_diario.py):
def get_string(number): result = 7 if number % 3 == 0: result = 'Python' elif number == 5: result = 'Diario' return result
Como vemos nuestro código se ha modificado significativamente.
Esto es debido a que el nuevo Test ha hecho que tengamos que
contemplar más casos, y por tanto tengamos que readaptar nuestro
código anterior. Esto es completamente normal cuando se hacen Tests,
por lo que no debemos sorprendernos si en ciertos momentos debemos
reescribir el código al igual que nos ha sucedido en este caso.
Esto no se considera una refactorización, ya que lo que hemos hecho es escribir el mínimo código posible para que el test pase. En este caso el mínimo código posible son varias líneas.
Iteraciones 6 y 7
1.- Escribir un Test que falle (Red)
Para no hacer el post más largo vamos a agregar dos Tests y ver
como quedaría el algoritmo que hace que los tests pasen (test_python_diario_software.py):
def test_should_return_11_when_number_is_11(self): self.assertEqual(11, python_diario.get_string(11)) def test_should_return_41_when_number_is_41(self): self.assertEqual(41, python_diario.get_string(41))
2.- Escribir el mínimo código para que el Test pase (Green)
python_diario.py:
def get_string(number): result = number if number % 3 == 0: result = 'Python' elif number == 5: result = 'Diario' return result
Iteraciones 8 y 9
Vamos a agregar 2 test más y mostrar la
solución del algoritmo final. Recordad que los test se deben agregar de uno
en uno y pasar el ciclo de: Red, Green, Refactor:
1.- Escribir un Test que falle (Red)
test_python_diario_software.py:
def test_should_return_diario_when_number_is_10(self): self.assertEqual('Diario', python_diario.get_string(10)) def test_should_return_diario_when_number_is_20(self): self.assertEqual('Diario', python_diario.get_string(20))
2.- Escribir el mínimo código para que el Test pase (Green) y 3.- Refactorizar
python_diario.py:
def get_string(number): result = number if number % 3 == 0: result = 'Python' elif number % 5 == 0: result = 'Diario' return result
Conclusión
Hemos visto una función básica de como crear Tests en python
haciendo uso de la librería “unittest”.
Este ejemplo ha sido muy sencillo pero nos sirve para
familiarizarnos con el ciclo de:
- Escribir un Test que falla (Red)
- Escribir el mínimos código posible para hacer pasar el Test
(Green) - Refactorizar
Los Tests nos ayudan a enfocarnos en una cosa a la vez, y evitan
la tendencia de implementar en nuestro código funcionalidades extra
que tal vez nunca se lleguen a usar.
Cada Test debe considerarse como un requerimiento nuevo que nos
hace el “cliente”, y por tanto únicamente debemos implementar
aquello que el cliente nos pide.
Por ejemplo, si el cliente nos pide
un formulario de login sencillo que únicamente consiste de los
campos: email y password. Nos limitaremos a crear ese formulario.
Si
el cliente posteriormente nos pide que quiere agregar un “captcha”
al formulario, lo implementaremos y refactorizaremos nuestro código según los nuevos requerimientos.
Podéis ver el código de este ejercicio en esta dirección de Github: https://github.com/RubenDjOn/Python-FizzBuzz
Más información sobre unittesting en python: http://www.diveintopython3.net/unit-testing.html
----
Mi nombre es Rubén Hernández. En los últimos años he comenzado a programar en python, y es algo que me encanta.
Sigo aprendiendo nuevas cosas sobre este genial lenguaje, y aprovecharé este espacio para compartir mis nuevos descubrimientos, y afianzar otros conocimientos ya adquiridos.
Si tenéis dudas, sugerencias o queréis contar conmigo para algún proyecto, podéis contactar conmigo a través de mis redes sociales:
Twitter: @RubenDjOn
Google+: https://plus.google.com/+RubenHernandezA
Web Personal: rubendjon.com
-
Hola Ruben Hernandez, muy agradecida por el artículo.
Solo tengo que comentarte que en el primer bloque de código que muestras:
"""
import unittestimport python_diario
class TestPythonSoftware(unittest.TestCase):
def test_should_return_python_when_number_is_3(self):
self.assertEqual('Python', python_diario.get_string(3))if __name__ == '__main__':
unittest.main()
""""se tendría que sacar el if __name__==... de la clase para que pueda leer los test cuando ejecutas el archivo. Fue la solución que conseguí debido a que nunca se leían los test cuando ejecutaba el archivo.
Gabriela
Deja una respuesta
Gracias por el artículo, es muy entendedor.