Test Driven Development

Cómo abordar el desarrollo de software para Data Science usando Test Driven Development.

python
software-development
Autor/a

Francisco Alfaro

Fecha de publicación

25 de mayo de 2022

TDD


Esta sección busca dar señales de cómo abordar el desarrollo de software para Data Science usando Test Driven Development, una técnica ampliamente usada en otros rubros de la programación.

🔑 Nota: Les comparto de manera complementaria la charla que impartí en la conferencia de Python Chile sobre Test Driven Development (TDD).

¿Qué es el Test Driven Development?

En palabras simples, el desarrollo guiado por pruebas pone las pruebas en el corazón de nuestro trabajo. En su forma más simple consiste en un proceso iterativo de 3 fases:

  • Red: Escribe un test que ponga a prueba una nueva funcionalidad y asegurate de que el test falla
  • Green: Escribe el código mínimo necesario para pasar ese test
  • Refactor: Refactoriza de ser necesario

Ejemplo sencillo

A modo de ejemplo, vamos a testear la función paridad, que determina si un número natural es par o no.

Lo primero que se debe hacer es crear el test, para ello se ocupará la librería pytest.

Nota: No es necesario conocer previamente la librería pytest para entender el ejemplo.

@pytest.mark.parametrize(
    "number, expected",
    [
        (2, 'par'),
])
def test_paridad(number, expected):
    assert paridad(number) == expected

El test nos dice que si el input es el número 2, la función paridad devuelve el output 'par'. Cómo aún no hemos escrito la función, el test fallará (fase red).

========= test session starts ============================================ 
platform linux -- Python 3.8.10, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/fralfaro/PycharmProjects/ds_blog
plugins: anyio-3.3.0
collected 1 item                                                                                                                                                                          

temp/test_funcion.py F                                              [100%]
========= 1 failed in 0.14s  ===============================================

Ahora, se escribe la función paridad (fase green):

def paridad(n:int)->str:
    """
    Determina si un numero natural es par o no.
    
    :param n: numero entero
    :return: 'par' si es el numero es par; 'impar' en otro caso
    """
    return 'par' if n%2==0 else 'impar'

Volvemos a correr el test:

========= test session starts ============================================ 
platform linux -- Python 3.8.10, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/fralfaro/PycharmProjects/ds_blog
plugins: anyio-3.3.0
collected 1 item                                                                                                                                                                          

temp/test_funcion.py .                                              [100%]
========= 1 passed in 0.06s  ===============================================

Hemos cometido un descuido a proposito, no hemos testeado el caso si el número fuese impar, por lo cual reescribimos el test (fase refactor)

@pytest.mark.parametrize(
    "number, expected",
    [
        (2, 'par'),
        (3, 'impar'),
])
def test_paridad(number, expected):
    assert paridad(number) == expected

y corremos nuevamente los test:

========= test session starts ============================================ 
platform linux -- Python 3.8.10, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/fralfaro/PycharmProjects/ds_blog
plugins: anyio-3.3.0
collected 2 items                                                                                                                                                                          

temp/test_funcion.py ..                                              [100%]
========= 2 passed in 0.06s  ===============================================

Listo, nuestra función paridad ha sido testeado correctamente!.

¿Porqué debería usarlo?

Existen varias razones por las que uno debería usar TDD. Entre ellas podemos encontrar: - Formular bien nuestros pensamientos mediante la escritura de un test significativo antes de ponernos a solucionar el problema nos ayuda a clarificar los límites del problema y cómo podemos resolverlo. Con el tiempo esto ayuda a obtener un diseño modular y reusable del código. - Escribir tests ayuda la forma en que escribimos código, haciéndolo más legible a otros. Sin embargo, no es un acto de altruismo, la mayoría de las veces ese otro es tu futuro yo. - Verifica que el código funciona de la manera que se espera, y lo hace de forma automática. - Te permite realizar refactoring con la certeza de que no has roto nada. - Los tests escritos sirven como documentación para otros desarrolladores. - Es una práctica requerida en metodologías de desarrollo de software agile.

Evidencia empírica

El 2008, Nagappan, Maximilien, Bhat y Williams publicaron el paper llamado Realizing Quality Improvement Through Test Driven Development - Results and Experiences of Four Industrial Teams, en donde estudiaron 4 equipos de trabajo (3 de Microsoft y 1 de IBM), con proyectos que variaban entre las 6000 lineas de código hasta las 155k. Estas son parte de sus conclusiones:

Todos los equipos demostraron una baja considerable en la densidad de defectos: 40% para el equipo de IBM, y entre 60-90% para los equipos de Microsoft.

Como todo en la vida, nada es gratis:

Incremento del tiempo de desarrollo varía entre un 15% a 35%.

Sin embargo

Desde un punto de vista de eficacia este incremento en tiempo de desarrollo se compensa por los costos de mantención reducidos debido al incremento en calidad.

Además, es importante escribir tests junto con la implementación en pequeñas iteraciones. George y Williams encontraron que escribir tests después de que la aplicación está mas o menos lista hace que se testee menos porque los desarrolladores piensan en menos casos, y además la aplicación se vuelve menos testeable. Otra conclusión interesante del estudio de George y Williams es que un 79% de los desarrolladores experimentaron que el uso de TDD conlleva a un diseño más simple.

¿Puedo usar TDD siempre?

No, pero puedes usarlo casi siempre. El análisis exploratorio es un caso en que el uso de TDD no hace sentido. Una vez que tenemos definido el problema a solucionar y un mejor entendimiento del problema podemos aterrizar nuestras ideas a la implementación vía testing.

Librerías disponibles

Acá listamos algunas librerías de TDD en Python: - unittest: Módulo dentro de la librería estándar de Python. Permite realizar tests unitarios, de integración y end to end. - doctest: Permite realizar test de la documentación del código (ejemplos: Numpy o Pandas). - pytest: Librería de testing ampliamente usada en proyectos nuevos de Python. - nose: Librería que extiende unittest para hacerlo más simple. - coverage: Herramienta para medir la cobertura de código de los proyectos. - tox: Herramienta para facilitar el test de una librería en diferentes versiones e intérpretes de Python. - hypothesis: Librería para escribir tests vía reglas que ayuda a encontrar casos borde. - behave: Permite utilizar Behavior Driven Development, un proceso de desarrollo derivado del TDD.

Referencias