Ideas en bits

Ejercitar la Mente Parte II

Siguiendo con el post de ayer sobre ejercitar la mente me quede pensando un poco en como hacer un pequeño refactor, y de paso usar algo que hace rato quiero aprender: Property Based Testing

Este tema lo escuche la primera vez por una charla en una PyAr de Pablo Fernandez, la cual es muy interesante y detallada, y la dejo a continuación:

[YouTube 5Z8w6b6Dlvk]

Disclaimer

Edito este post haciendo un warning temprano: al final en los ejemplos de código hay un error, y lo explico en este artículo. Asi que si estas leyendo esto, por favor tambien lee la otra parte.

Seguimos!

Que es Property Based Testing?

Haciendo una pequeña síntesis encontré esta definición que me gustó

Property-based testing is generative testing. You do not supply specific example inputs with expected outputs as with unit tests. Instead, you define properties about the code and use a generative-testing engine to create randomized inputs to ensure the defined properties are correct.

Es decir, vamos a testear un codigo definiendo un conjunto de elementos posibles y obteniendo inputs random usando un motor.

Esto tiene infinidades de uso, y te abre la cabeza para pensar los tests de otra manera, y aprovechando el ejemplo simple de mi post anterior, decidí utilizarlo para aprender un poco más.

Refactor

Vamos primero a ordenar un poco el método inicial y hacerlo generico en el sentido que podemos enviar como parámetro cual es el dia de la semana que queremos verificar con la fecha actual para saber si es el primero del mes:

def is_first_weekday_of_month(weekday: int) -> bool:
    """Determine if today is the first weekday of the month.

    :param weekday: weekday we want to identify (starting as monday -> 0)
    """
    today = datetime.today()
    current_weekday = today.weekday()
    day = today.day

    return current_weekday == weekday and day <= 7

Regular Unit Tests

Probemos el set de tests que habíamos armado ayer para comprobar que todo sigue funcionando:

import pytest
from freezegun import freeze_time


@pytest.mark.parametrize(
    "date,expected",
    [
        ("2021-10-04 03:21:34", True),
        ("2021-10-11 03:21:34", False),
        ("2021-07-15 03:21:34", False),
        ("2021-07-26 03:21:34", False),
        ("2021-02-01 03:21:34", True),
        ("2021-12-06 03:21:34", True),
        ("2021-06-07 03:21:34", True),
    ],
)
def test_is_first_monday_of_month(date, expected):

    with freeze_time(date):
        assert dates_utils.is_first_weekday_of_month(0) == expected
$ pytest -k test_is_first_monday_of_month

================= 7 passed ==============

Hypothesis

Ahora vamos a probar a nuestro método, pero esta vez utilizando el framework Hypothesis.

Instalación super sencilla:

pip install hypothesis

En este caso voy a utilizar un generador de fechas, el cual por defecto va a usar 100 fechas al azar tomando el valor datetime.datetime.min y datetime.datetime.max como extremos.

Lo que voy a hacer al tener esa fecha random es también obtener el mismo dia pero de la semana pasada, y en cada ejecución voy a congelar el tiempo con freezegun y verificar en ambos casos si el dia es el primero de la semana.

Si alguno de los 2 resulta ser el primer dia de la semana, eso quiere decir que el mismo dia de la semana pasada (en caso de ser la fecha original), o de la siguiente semana (en caso de ser la fecha anterior), no deberían ser el primer dia del mes.

what

Sí, mientras lo voy leyendo siento que es un quilombo por lo mal que lo redacte, asi que vamos con un ejemplo:

Supongamos que el dato random que elije el motor es: 3 de Febrero 2021, con esta fecha sabemos que:

  1. Es un dia miércoles
  2. Es el primer miércoles del mes

Entonces nuestro test va a llamar a nuestro método, simulando que hoy es 10 de febrero, y va a usar nuestro metodo para averiguar 2 cosas:

  1. Hoy si es el primer miércoles del mes?
  2. Hace una semana, era el primer miércoles del mes?

Y con estos datos nuestro assert va a ser del estilo:

  1. Hoy es el primer dia del mes, por lo tanto el mismo día de la semana pasada no lo es.

De igual manera va a ser el test para otros 2 casos:

  1. La fecha original no es el primero de mes, pero la anterior si.
  2. Ni la fecha original, ni la de la anterior semana son primero de mes.

Y nuestro test queda mas o menos asi:

from freezegun import freeze_time
from hypothesis import given, settings
from hypothesis import strategies as st


@given(st.dates())
def test_is_first_weekday_of_month(date):

    with freeze_time(date):
        original_date_is_first = dates_utils.is_first_weekday_of_month(date.weekday())

    previous_week = date - timedelta(weeks=1)
    with freeze_time(previous_week):
        previous_date_is_first = dates_utils.is_first_weekday_of_month(previous_week.weekday())

    if original_date_is_first:
        assert not previous_date_is_first
    elif previous_date_is_first:
        assert not original_date_is_first

Si ejecutamos este codigo mostrando las estadisticas, obtenemos lo siguiente:

$ pytest -k test_is_first_weekday_of_month --hypothesis-show-statistics

  - during generate phase (3.58 seconds):
    - Typical runtimes: 26-49 ms, ~ 1% in data generation
    - 100 passing examples, 0 failing examples, 0 invalid examples

  - Stopped because settings.max_examples=100

Es decir con esa simple configuración estamos generando 100 fechas aleatorias cada vez que corramos ese test.

bravo

Conclusiones

Para este caso particular es un overkill obviamente, pero me gusto encontrar un caso real donde pude aplicar un poquito de esta nueva forma de testear.

Notas

El post original tenia un error, ya lo corregí, pero explico todo esto en el siguiente.

Tal cual lo comenta Pablo en el video, hay que ir paso a paso y utilizar esto con coherencia (un gran poder, conlleva...)

Y como ultimo pensamiento, tal cual dije ayer, nunca subestimos los pequeños wins, que muchas veces nos abren el camino a cosas mas grandes.