TDD Python
TDD Python
Todo programador tiene el deber de producir código confiable y seguro; para ello debemos probar nuestro código y
asegurarnos que cumple con las especificaciones (hace lo que se supone que debe hacer). El TDD es una práctica que
involucra escribir las pruebas primero (Test First Development) y Refactorización (Refactoring) con el propósito de lograr
código limpio, confiable y que funcione correctamente.
EL TDD busca que los requisitos sean traducidos a pruebas, de este modo, cuando las pruebas pasen o sean satisfechas,
se garantizará que el software cumple con los requisitos que se han establecido.
Para aplicar correctamente el TDD en sus proyectos debes prestar atención al principio del
ciclo (Red, Green, Refactor).
En primer lugar, se escriben las pruebas y se verifica que fallan (Etapa Rojo). A continuación,
se desarrolla el código que hace que las pruebas pasen satisfactoriamente (Etapa Verde) y
seguidamente se refactoriza el código escrito (Etapa Refactorizar).
Este modo de proceder se basa en la idea de que nuestras mentes no son capaces de
perseguir dos metas de forma simultánea, es decir: el comportamiento correcto y la
estructura correcta. Entonces el ciclo RGR nos dice que nos concentremos primeramente en hacer que el código funcione
correctamente y solo después de lograr esto, hacer que este código ya funcional, tenga una estructura que le permita
sobrevivir en el tiempo.
Niveles de Pruebas:
• Unit testing: se realizan a la menor unidad de código posible, fragmentos indivisibles (funciones y/o métodos).
• Integration testing: comprueba las interacciones entre unidades de código estrechamente relacionadas.
• System testing: chequea el funcionamiento del sistema o de parte de este, cuando sus componentes han sido
puestos en su lugar. La respalda las pruebas unitarias y de integración.
• Acceptance testing: confirma que el programa hace lo que se espera que haga, según como nos han
especificado.
• Regression testing: cuando una parte de nuestro código que funcionaba correctamente deja de hacerlo, están
dirigidas a detectar donde radica el problema.
• Test-Driven Development: combina todas las anteriores.
“Antes de hacer, planifica tus pruebas” que te ayudara a planificar bien tus pruebas, sigue las leyes de la planificación en
TDD:
1. Solo puede escribir código de producción (para implementar una funcionalidad) si ya ha escrito su respectivo
código de prueba.
2. Solo puede escribir el código de prueba mínimo necesario que haga que el código de producción falle (Aplica
RGR).
3. Solo puede escribir el código de producción necesario para hacer que éste pase su código de prueba (Aplica
RGR).
• Los requerimientos del cliente quedan cumplidos al satisfacer todas las pruebas.
• Ofrece garantía y confianza al código.
• Constituye parte importante de la documentación y facilitan la comprensión de terceros
• Evita la escritura de código innecesario (funcionalidades no requeridas)
• Facilita el proceso de depuración con una detección temprana de errores
Conclusión incluye siempre los tiempos de tus pruebas en tus desarrollos, al final es un ahorro de tiempo al cliente y un
dolor de cabeza menos para la empresa y para usted.
TDD EN PYTHON
Hay muchos tipos de pruebas, pero la más importante es la prueba unitaria, pilar de las pruebas en la filosofía TDD,
lógicamente, las pruebas unitarias nunca pueden garantizar completamente el correcto funcionamiento de una porción
de código. No obstante, serán capaces de detectar gran cantidad de anomalías y ahorro de tiempo en la depuración.
Cuando vayas a desarrollar un módulo o un paquete (conjunto de funciones y clases) probablemente ya sabrás que antes
necesitas crear una prueba unitaria. El éxito de las pruebas unitarias radica en organizar un proyecto en múltiples unidades
con única responsabilidad, más información: ….
Resulta necesario contar con herramientas que nos permitan automatizar el proceso de correr o ejecutar las pruebas
unitarias, en Python existen buenas herramientas para lograr este objetivo como Unittest, para lo cual explicaremos su
enfoque:
ProjectFolder:
- project:
- __init__.py
- item.py
- tests:
- test_item.py
Se separan en dos carpetas, una agrupara las unidades de código de tu proyecto y en otra las pruebas por unidad de código
de tu proyecto.
Estructuras de los archivos Test:
import unittest
class Test(unittest.TestCase):
"""
The class inherits from unittest
"""
def setUp(self):
"""
This method is called before each test
"""
def tearDown(self):
"""
This method is called after each test
"""
## TESTS
def test_distance_measure(self):
"""
This method is called for test
"""
if __name__ == "__main__":
unittest.main()
El módulo unittest viene con la biblioteca estándar de Python. Proporciona una clase llamada TestCase, de la que se puede
derivar su clase. A continuación, el método setUp() prepara un dispositivo de prueba antes de cada prueba. Existen otros
métodos opcionales como tearDown() que se llaman después de ejecutar cada prueba. Veamos con un ejemplo real:
El siguiente código de ejemplo para el cálculo de distancia entre dos coordenadas, seguimos las reglas de planificación
de pruebas, creamos una prueba y luego su código, el mínimo, para que falle y para que después según sus fallos se
refactorice para que pasen la pruebas.
Clase prueba:
from unittest import TestCase
from MeasureDistance import MeasureDistance
class MeasureDistanceTest(TestCase):
def setUp(self):
def test_distance_measure(self):
origin = [None, None]
destination = [None, None]
self.__measure_distance.setElipsoide(6378137.00)
self.assertEqual(6378137.00, self.__measure_distance.getElipsoide())
#self.calc_distance.measure(origin, destination)
self.assertIsInstance(self.__measure_distance.measure(origin, destination), float)
Ahora teniendo en cuenta que vamos a probar, escribimos su código, el mínimo para que falle.
Clase que mide distancias:
import math
class MeasureDistance:
def __init__(self,):
# clase que calcula la distancia de dos coordenadas
def setElipsoide(self, elipsoide):
# cambia la medida del radio de la tierra
if isinstance(elipsoide, float):
self.__elipsoide = elipsoide
def getElipsoide(self):
return self.__elipsoide
En las primeras dos líneas de la clase prueba, importamos el módulo unittest necesario para crear las pruebas unitarias y
a continuación el propio módulo que llama la clase que queremos probar “MeasureDistance”.
Luego creamos la clase MeasureDistanceTest, unidad de prueba de nuestra clase, dentro de ella creamos tantos métodos
como funciones que queremos probar de la clase; todos los métodos que comiencen con el nombre test serán ejecutados.
Aquí están las partes relevantes de nuestra clase; utilizo el método setUp() para preparar nuestro entorno, creo una nueva
instancia MeasureDistance y la almaceno en la variable measure_distance para que esté disponible en cada prueba.
def setUp(self):
self.__measure_distance = MeasureDistance()
El siguiente paso es escribir métodos de prueba específicos para probar el código de la clase; para comprobar que estamos
diseñando bien nuestros métodos de prueba, podemos seguir la estructura estándar:
Por ejemplo, el método. setElipsoide() de la clase MeasureDistance no devuelve nada, pero cambia el valor interno
estableciendo el valor del elipsoide diferente a Nulo.
El método assertEqual() proporcionado por la clase base TestCase se utiliza aquí para verificar que al llamar getElipsoide()
funcionó como se esperaba y devuelva un valor esperado.
La documentación provee una tabla con el resto de las funciones de unittest y su respectiva operación.
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a, b)
Ahora aplicando el ciclo RGR vamos a automatizar esta prueba; preparemos una prueba que falle para ello debemos
saber cuál es el resultado esperado (valor de retorno tipo float), llamamos el código de prueba que es el método
“measure” el cual recibe los valores de dos coordenadas, vamos a enviar valores que pueden comúnmente hacer que
nuestro código falle, ejemplo:
def test_distance_measure(self):
origin = [None, None]
destination = [None, None]
self.__measure_distance.setElipsoide(6378137.00)
self.assertEqual(6378137.00, self.__measure_distance.getElipsoide())
Resultado de la prueba:
ERROR: test_distance_measure (TestMeasureDistance.MeasureDistanceTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\IntersectsScripts\test1\TestMeasureDistance.py", line 16, in test_distance_measure
self.__measure_distance.measure(origin, destination)
File "C:\IntersectsScripts\test1\MeasureDistance.py", line 22, in measure
dlat = self.__hex_to_rad(lat2-lat1)
TypeError: unsupported operand type(s) for -: 'NoneType' and 'NoneType'
----------------------------------------------------------------------
Ran 1 test in 0.005s
FAILED (errors=1)
En realidad, hay dos pruebas aquí. La primera prueba es asegurarse de que el valor del radio de la tierra para la medición
sea el que realmente enviamos por parámetro y luego comprobamos que el código del método de medir distancia.
Como vemos fallan nuestro código; para probar el manejo de errores en la prueba, utilizaremos el método
self.assertRaises(), este método nos ayuda a manejar las excepciones y validarlas, para ello necesitamos pasar como
argumento el tipo de excepción que esperamos validar en la prueba y el método que estamos probando.
def test_distance_measure(self):
origin = [None, None]
destination = [None, None]
self.__measure_distance.setElipsoide(6378137.00)
self.assertEqual(6378137.00, self.__measure_distance.getElipsoide())
Resultado de la prueba:
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Process finished with exit code 0
Ahora sigamos con el segundo paso del ciclo RGR, después de planificar las pruebas que harían que el método de nuestra
clase falle, ahora ajustamos el código para que las mismas pruebas pasen satisfactoriamente (Etapa Verde), por eso
cambiamos el código del método measure() de la clase MeasureDistance para que pasen las pruebas:
Código Método de pruebas:
def test_distance_measure(self):
origin = [None, None]
destination = [None, None]
self.measure_distance.setElipsoide(6378137.00)
self.assertEqual(6378137.00, self.measure_distance.getElipsoide())
try:
if not (isinstance(origin, list)) and not (isinstance(destination, list)) and \
not (len(origin) == 2 and not len(destination) == 2):
raise ValueError("The values aren't valids.")
Resultado de la prueba:
----------------------------------------------------------------------
Ran 1 test in 21.736s
OK
Process finished with exit code 0
Al haber pasado todas las pruebas del método ahora se refinan o refactoriza el código, por ejemplo, queremos inyectar
las dependencias a la clase para que esta calcule las distancia según una unidad de medida ya sea en metros o kilómetros;
para eso vamos a refactorizar el código, y se vuelven hacer las pruebas cumpliendo la etapa del ciclo RGR aplicando el
enfoque TDD.
Clase prueba TestMeasureDistance.py
#Singleton Class
class DistanceUnitMeasure(object):
radius = 0
_instances = {}
"Constructor"
def __new__(class_, *args, **kwargs):
if class_ not in class_._instances:
class_._instances[class_] = super(DistanceUnitMeasure, class_).__new__(class_, *args,
**kwargs)
class_._instances[class_].__init()
return class_._instances[class_]
def __init(self):
self.radius = 6378137.00
class MeasureDistanceTest(TestCase):
def setUp(self):
distance_unit = DistanceUnitMeasure()
self.measure_distance = MeasureDistance(distance_unit.calculate_distance_mt)
def test_distance_measure(self):
origin = [None, None]
destination = [None, None]
self.measure_distance.setElipsoide(6378137.00)
self.assertEqual(6378137.00, self.measure_distance.getElipsoide())
import math
class MeasureDistance:
elipsoide = None
def getElipsoide(self):
return self.elipsoide
try:
if not (isinstance(origin, list)) and not (isinstance(destination, list)) and \
not (len(origin) == 2 and not len(destination) == 2):
raise ValueError("The values aren't valids.")
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
Process finished with exit code 0
Hasta aquí hemos visto una introducción de Unittest en un enfoque TDD, pero en una segunda parte explicare como
Webdriver/Selenium es un complemento cuando se trata de pruebas en entornos de desarrollo para ambiente web,
además profundizare en la estructura de Unittest y otras funcionalidades.