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.
- Escribir un test que falle (Red)
- Hacer que el test pase (sea válido) (Green)
- Refactorizar (Opcional) (Refactor)
Con estas 3 reglas en mente vamos a proceder a realizar nuestro
ejercicio haciendo uso de Tests.
Y meter el siguiente código:
El código consiste de las siguientes partes:
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.pyimport 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:
http://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
>> 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'
Como vemos, el Test falla, esto es normal, ya que una de las buenas prácticas del TDD es la de escribir primero el
Test, antes de escribir cualquier línea de código en nuestro
programa. Es por ello que nosotros hemos escrito primero el Test.
En “python_diario.py” escribimos el siguiente código: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”.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.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”.
>> 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'
Ejecutamos los Tests:
>> python3
test_python_diario_software.py
..
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
Iteración 4
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'
>> python3 test_python_diario_software.py
..
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
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
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
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
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
Gracias por el artículo, es muy entendedor.
ResponderEliminarHola Ruben Hernandez, muy agradecida por el artículo.
ResponderEliminarSolo tengo que comentarte que en el primer bloque de código que muestras:
"""
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()
""""
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