Cuando estamos desarrollando un programa, si hicimos las cosas con responsabilidad y ejecutamos el programa, hicimos algunas pruebas, etc, podemos afirmar que lo que hicimos, con mayor o menor seguridad, funciona. A partir de ese momento y hasta la próxima vez que se modifique el código, o que alguien encuentre una falla en el sistema (bug) podemos seguir manteniendo esa afirmación.
Supongamos que después de tiempo viene el usuario y nos pide un cambio en el funcionamiento del programa... una vez que modificamos el código para incorporar ese requerimiento del usuario tenemos que probar nuevamente todo el programa para asegurarnos que con esa modificación no rompimos nada. En programas chicos esto no tiene un gran impacto, pero si pensamos en un programa que actualmente se está usando y se estuvo desarrollando por años, un pequeño cambio podría requerir semanas de pruebas. Y es por eso que no todo el código que escribimos en nuestra vida profesional está destinado a ser ejecutado por el usuario final, gran parte, está destinado a asegurar que nuestro código funciona como nosotros decimos que funciona.
Existen varias formas de probar automáticamente nuestro código:
Pero por el momento nos centraremos en lo que son las pruebas unitarias.
Las pruebas unitarias son catalogadas de caja blanca, ya que es necesario conocer el código para poder escribirlas.
Estas pruebas tienen que ser:
Las ventajas de realizar pruebas unitarias son:
Las etapas de una prueba unitaria son:
Al momento de desarrollarlas:
Es importante tener en cuenta no sólo los casos felices, sino también los que tienen que mostrar algún mensaje de error para asegurarnos que el código maneje esos errores como corresponde.
En la versión 2.1 de python se incluyó el módulo unittest que es uno de los que usualmente se utiliza para implementar estas pruebas.
La estructura de un archivo que contenga las pruebas debe ser:
# encoding: utf-8
import unittest # Importar el módulo unittest
# Crear una clase que herede de unittest.TestCase
class TestStringMethods(unittest.TestCase):
# Definir un método que comience con test
def test_upper_of_foo_will_return_foo_in_uppercase(self):
# Setup
target = 'foo'
expected_result = 'FOO'
# Exercise
result = target.upper()
# Verify
self.assertEqual(result, expected_result)
# Definir un método que comience con test
def test_isupper_of_upper_target_will_return_true(self):
# Setup
target = 'FOO'
# Exercise
result = target.isupper()
# Verify
self.assertTrue(result)
# Definir un método que comience con test
def test_isupper_of_capitalize_target_will_return_false(self):
# Setup
target = 'Foo'
# Exercise
result = target.isupper()
# Verify
self.assertFalse(result)
# Esto es opcional, pero si se quiere ejecutar los tests
# como python test_de_strings.py es necesario.
if __name__ == '__main__':
unittest.main()
Y si suponemos que el archivo se llama test_de_strings.py
y lo ejecutamos con el comando python test_de_strings.py
nos mostrará en la consola:
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
Y si a la clase TestStringMethods
le agregamos el siguiente método:
def test_isupper_of_lower_target_will_return_true(self):
# Setup
target = 'foo'
# Exercise
result = target.isupper()
# Verify
self.assertTrue(result)
Va a fallar este nuevo test, ya que result
valdrá False.
.F..
======================================================================
FAIL: test_isupper_of_lower_target_will_return_true (__main__.TestStringMethods)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_de_strings.py", line 50, in test_isupper_of_lower_target_will_return_true
self.assertTrue(result)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 4 tests in 0.001s
FAILED (failures=1)
Otra forma de ejecutar estos tests es, parado en la misma carpeta donde se encuentra el archivo, ejecutando el comando python -m unittest discover
y en ese caso no es necesario poner al final del archivo las líneas:
if __name__ == '__main__':
unittest.main()
En realidad, no es necesario que se encuentren en la misma carpeta, lo que tiene que pasar es que se encuentre dentro del mismo paquete. Y eso en python para eso se usan los archivos __init__.py
.
Supongamos que tenemos que hacer una función que parsea una línea de un archivo de texto sabiendo que es un archivo CSV (por lo que cada campo estará separado por una coma) y el formato es:
numero_de_partido,goles_local,goles_visitante # comentario
Donde:
numero_de_partido
: es un número entero mayor a 1 (no tiene límite superior)goles_local
y goles_visitante
: son los goles convertidos por cada uno de los equiposDicha función tiene que retornar un diccionario con los campos provistos por el archivo.
Si pudieramos asumir que el archivo siempre tendrá líneas válidas, una posible solución podría ser:
def parsear_linea_prode(linea):
'''Función que no parsea una línea de un archivo
CSV con los resultados de un partido.
return: Diccionario con las claves numero_de_partido,
goles_local y goles_visitante.
'''
sin_comentario = linea.partition('#')[0]
sin_espacios = sin_comentario.strip()
id_partido, goles_loc, goles_vis = sin_espacios.split(',')
resultado = {
'numero_partido': int(id_partido),
'goles_local': int(goles_loc),
'goles_visitante': int(goles_vis)
}
return resultado
Para asegurarnos que nuestro código funciona correctamente podríamos agregar los siguientes test:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import unittest
from prode import parsear_linea_prode
class TestParsearLineasFixture(unittest.TestCase):
def test_parsear_linea_prode_parsea_bien_la_primer_linea(self):
# Setup
linea = '1,0,0'
resultado_esperado = {
'numero_partido': 1,
'goles_local': 0,
'goles_visitante': 0
}
# Exercise
resultado = parsear_linea_prode(linea)
# Verify
self.assertEquals(resultado, resultado_esperado)
def test_parsear_linea_prode_ignora_el_comentario_despues_del_numeral(self):
# Setup
linea = '1,0,0 # Chile vs Ecuador'
resultado_esperado = {
'numero_partido': 1,
'goles_local': 0,
'goles_visitante': 0
}
# Exercise
resultado = parsear_linea_prode(linea)
# Verify
self.assertEquals(resultado, resultado_esperado)
def test_parsear_linea_prode_ignora_el_enter_al_final_de_la_linea(self):
# Setup
linea = '1,0,0 # Chile vs Ecuador\n'
resultado_esperado = {
'numero_partido': 1,
'goles_local': 0,
'goles_visitante': 0
}
# Exercise
resultado = parsear_linea_prode(linea)
# Verify
self.assertEquals(resultado, resultado_esperado)
def test_parsear_linea_para_valores_mayores_a_10_tambien_funciona(self):
# Setup
linea = '999,123,432 # Chile vs Ecuador\n'
resultado_esperado = {
'numero_partido': 999,
'goles_local': 123,
'goles_visitante': 432
}
# Exercise
resultado = parsear_linea_prode(linea)
# Verify
self.assertEquals(resultado, resultado_esperado)
if __name__ == '__main__':
unittest.main()
Pero qué pasa si después nos agregan un requerimiento en el que dicen que, en
def test_parsear_linea_prode_retorna_un_diccionario_vacio_cuando_le_pasan_una_linea_vacia(self):
# Setup
linea = ''
resultado_esperado = {}
# Exercise
resultado = parsear_linea_prode(linea)
# Verify
self.assertEquals(resultado, resultado_esperado)
def test_parsear_linea_prode_retorna_un_diccionario_vacio_cuando_le_pasan_4_valores(self):
# Setup
linea = '1,2,3,4'
resultado_esperado = {}
# Exercise
resultado = parsear_linea_prode(linea)
# Verify
self.assertEquals(resultado, resultado_esperado)
def test_parsear_linea_prode_retorna_un_diccionario_vacio_cuando_le_pasan_2_valores(self):
# Setup
linea = '1,2'
resultado_esperado = {}
# Exercise
resultado = parsear_linea_prode(linea)
# Verify
self.assertEquals(resultado, resultado_esperado)
def test_parsear_linea_prode_retorna_un_diccionario_vacio_cuando_le_pasan_comentario_sin_numeral(self):
# Setup
linea = '1,2,3 comentario'
resultado_esperado = {}
# Exercise
resultado = parsear_linea_prode(linea)
# Verify
self.assertEquals(resultado, resultado_esperado)
def test_parsear_linea_prode_retorna_un_diccionario_vacio_cuando_le_pasan_una_letra(self):
# Setup
linea = '1,a,3'
resultado_esperado = {}
# Exercise
resultado = parsear_linea_prode(linea)
# Verify
self.assertEquals(resultado, resultado_esperado)
Nosetest y py.test.
Para más información se puede ver: http://docs.python-guide.org/en/latest/writing/tests/