Hace relativamente poco, nos hemos encontrado con la clásica situación con la que más de uno/a se siente identificado. Cuando estamos escribiendo código, nos sugieren una mejora y pensamos: ¡Ah, es verdad, no se nos había ocurrido!
Entrar a ver código de los tests, y encontrarte con un montón de datos en la sección del given, puede ser doloroso. Datos que meten ruido, o que te lían más de lo que deberían, ya que muy probablemente la lógica de tu programa no hace nada con ellos, sólo los añade y/o se comprueba que están en la salida.
Por poner un ejemplo, me invento los datos:
def test_create_report_of_available_products_should_return_available_products(self, config, config_key):
inventory_dataframe = pd.DataFrame(
[
{
"name": "Kalia Vanish!",
"create_date": "irrelevant-date",
"is_out_of_date": "irrelevant-date",
"color": "irrelevant-color",
"id_of_product": 1,
"internal_id": 111,
"barcode": 7777777,
"availability": "Yes"
},
{
"name": "Kalia Vanish!",
"create_date": "irrelevant-date",
"is_out_of_date": "irrelevant-date",
"color": "irrelevant-color",
"id_of_product": 2,
"internal_id": 222,
"barcode": 7777777,
"availability": "No"
},
{
"name": "Kalia Vanish!",
"create_date": "irrelevant-date",
"is_out_of_date": "irrelevant-date",
"color": "irrelevant-color",
"id_of_product": 3,
"internal_id": 333,
"barcode": 7777777,
"availability": "No"
},
]
).astype("string")
expected_report = pd.DataFrame(
[
{
"name": "Kalia Vanish!",
"create_date": "irrelevant-date",
"is_out_of_date": "irrelevant-date",
"color": "irrelevant-color",
"id_of_product": 1,
"internal_id": 111,
"barcode": 7777777,
"availability": "Yes"
}
]).astype("string")
generated_report = create_report_of_available_products_from(inventory_dataframe).
assert_that(generated_report).is_equal_to(expected, check_like=True)
Lo primero que se te viene a la mente seguro es:
Uffffffff, ¡a ver qué datos tiene este objeto…?
Y echas la tarde leyendo los campos de ese conjunto, e intentando entender por qué están ahí todos esos datos y qué hacen.
Pero realmente, lo que tu lógica está haciendo, es filtrar aquellos productos que están disponibles de los que no. Imaginémonos que, la lógica de create_report_of_available_products_from sólo usa las propiedades availability e internal_id. El resto de información no se usa en el método, y no aporta nada a este test en particular… Peeeero, como “siempre se ha hecho así en los otros tests”, tendemos a pecar de copy-paste y realmente nos perjudica.
Para el caso anterior, nos basta con quitar del given, aquellas propiedades que no se usen en el método que queremos testear:
def test_create_report_of_available_products_should_return_available_products(self, config, config_key):
inventory_dataframe = pd.DataFrame(
[
{
"internal_id": 111,
"availability": "Yes"
},
{
"internal_id": 222,
"availability": "No"
},
{
"internal_id": 333,
"availability": "No"
},
]
).astype("string")
expected_report = pd.DataFrame(
[
{
"internal_id": 111,
"availability": "Yes"
}
]).astype("string")
generated_report = create_report_of_available_products_from(inventory_dataframe).
assert_that(generated_report).is_equal_to(expected, check_like=True)
Mucho mejor, ¿no? Pero… ¿Qué pasa con los datos que hemos borrado? ¿Dónde están? ¡Son necesarios al 100% para el reporte! Correcto, hay que añadirlos al test, pero de una manera sutil para que no añada ruido y así hacemos que en nuestro test de una lectura, sepamos qué campos son los que estamos utilizando:
def test_create_report_of_available_products_should_return_available_products(self, config, config_key):
inventory_dataframe = pd.DataFrame(
[
{
"internal_id": 111,
"availability": "Yes"
},
{
"internal_id": 222,
"availability": "No"
},
{
"internal_id": 333,
"availability": "No"
},
]
).astype("string")
expected_report = pd.DataFrame(
[
{
"internal_id": 111,
"availability": "Yes"
}
]).astype("string")
expected_report_with_extra_data = self._fill_irrelevant_test_fields(expected_report)
generated_report = create_report_of_available_products_from(inventory_dataframe).
assert_that(generated_report).is_equal_to(expected_report_with_extra_data, check_like=True)
El método self._fill_irrelevant_test_fields (se puede mejorar el nombre, es un ejemplito) lo crearías en la misma clase de tus tests, y ahí haces algo del estilo:
def _fill_irrelevant_test_fields(self, expected: pd.DataFrame) -> pd.DataFrame:
expected["name"] = "irrelevant-name"
expected["create_date"] = "irrelevant-date"
expected["is_out_of_date"] = "irrelevant-date"
expected["id_of_product"] = "irrelevant-fund-name"
expected["barcode"] = "irrelevant-barcode"
return expected.astype("string")
¿Te parece bonito pero quieres llevarlo un paso más allá? No te preocupes, podemos crearnos un builder para nuestros objetos:
def ItemBuilder(
name: str = "irrelevant-name",
create_date: str = "irrelevant-create_date",
is_out_of_date: bool = False,
id_of_product: str = "irrelevant-id_of_product",
barcode: str = "irrelevant-barcode",
internal_id: str = "irrelevant-internal_id",
availability: bool = False
) -> dict:
return {
"name": name,
"create_date": create_date,
"is_out_of_date": is_out_of_date,
"id_of_product": id_of_product,
"barcode": barcode,
"internal_id": internal_id,
"availability": availability
}
De esta manera nuestro test nos quedaría finalmente así:
def test_create_report_of_available_products_should_return_available_products(self, config, config_key):
inventory_dataframe = pd.DataFrame(
[
ItemBuilder(internal_id="111", availability="Yes"),
ItemBuilder(internal_id="222", availability="No"),
ItemBuilder(internal_id="333", availability="No")
])
expected_report = pd.DataFrame(
[
ItemBuilder(internal_id="111", availability="Yes")
])
generated_report = create_report_of_available_products_from(inventory_dataframe).
assert_that(generated_report).is_equal_to(expected_report, check_like=True)
¿Qué logramos con esto? Quitarnos el método de self._fill_irrelevant_test_fields, porque ya nuestro ItemBuilder nos añade por defecto todos los campos necesarios del objeto con un valor irrelevante. En nuestro test, al llamar al ItemBuilder, le pasamos solo aquellos campos que queremos utilizar y nos queda un test limpio, ordenado, funcional y fácilmente legible.
Muchas gracias a Sara Revilla, por sugerir en el artículo original unas cuantas mejoras y la parte final del ItemBuilder.
Está muy guay este pequeño tip que nos enseñaron en Clarity.AI, tras ver nuestros tests. La verdad es que ayuda muchísimo al entrar a ver código de alguien, y que esté todo limpio, separado y con lo justo y necesario para que lo entiendas.
¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.
Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?
Pero espera 🖐 que tenemos un conflicto interno. A nosotros las newsletter nos parecen 💩👎👹 Por eso hemos creado la LEAN LISTA, la primera lista zen, disfrutona y que suena a rock y reggaeton del sector de la programación. Todos hemos recibido newsletters por encima de nuestras posibilidades 😅 por eso este es el compromiso de la Lean Lista