Cuando estamos creando nuestros tests, es importante diferenciar qué es importante y qué es relevante. Hoy vamos a ver qué significa cada cosa y cómo mejorar notablemente nuestros tests.
Importante
Cualidad de lo importante: aquello que es muy conveniente, interesante o necesario.
En el contexto del testing, yo considero importante todo aquello que debe estar presente para que el test pueda ejecutarse correctamente, pero cuyo contenido específico no influye en el resultado del mismo. Es decir, son elementos necesarios para ejecutar el test, pero no son el foco de lo que estamos validando.
Por ejemplo, si estamos probando una funcionalidad para crear usuarios, puede que necesitemos crear un objeto User
.
Este objeto es importante para que el sistema funcione, sin él el test ni siquiera se ejecutaría, pero si la creacion no depende de los atributos del usuario, entonces no nos importa qué datos contiene, puede ser un usuario genérico, sin información relevante.
Lo importante hay que crearlo, pero no importa tanto el como. Basta con que esté ahí para que el test funcione. Tratarlo como si fuera relevante solo añade ruido a los tests y dificulta su lectura y mantenimiento.
Relevante
Sobresaliente, destacado.
Un elemento puede ser relevante además de importante. Es decir, no solo es necesario para que el test funcione, sino que su forma y contenido influyen directamente en el resultado de la prueba.
Cuando algo es relevante, los datos con los que lo construimos sí importan: cambiar sus valores puede alterar el comportamiento del sistema o invalidar la lógica que estamos verificando.
Siguiendo con el ejemplo anterior, si estamos testeando una función que calcula descuentos en función del tipo de usuario entonces los atributos del objeto User
(como por ejemplo su edad, ciudad o pais) sí son relevantes.
No basta con crear cualquier usuario: debemos crear uno con características específicas que nos permitan comprobar que la lógica del descuento se aplica correctamente.
Lo relevante hay que construirlo con intención y validarlo con precisión. Estos datos son la clave de lo que estamos testando y merecen ser relevantes en los tests.
Importante, pero no relevante
Veamos un caso práctico de información importante, pero no relevante.
Imaginemos que queremos escribir un test que valide la creación de usuarios a través de una API. Algo así:
class TestUsersRouter:
def test_create_user(self) -> None:
user_id = uuid4()
name = "Peter"
age = 30
email = "foo@bar.com"
city = "Springfield"
country = "Spain"
payload = {
"id": user_id.hex,
"name": name,
"age": age,
"email": email,
"city": city,
"country": country
}
user = User(
id=user_id,
name=name,
age=age,
email=email,
city=city,
country=country
)
command = CreateUserCommand(user)
client = TestClient(app)
handler = Mimic(Spy, CreateUserCommandHandler)
app.dependency_overrides[_get_create_users_command_handler] = lambda: handler
response = client.post("/api/v1/users", json=payload)
expect(response.status_code).to(equal(CREATED))
expect(handler.execute).to(have_been_called_with(command))
Para ello, necesitamos crear una instancia de la clase User
y un payload en formato dict
para enviar en la llamada a la API.
...
user_id = uuid4()
name = "Peter"
age = 30
email = "foo@bar.com"
city = "Springfield"
country = "Spain"
payload = {
"id": user_id.hex,
"name": name,
"age": age,
"email": email,
"city": city,
"country": country
}
user = User(
id=user_id,
name=name,
age=age,
email=email,
city=city,
country=country
)
...
ESTAS 21 LÍNEAS DE CÓDIGO SON LO QUE YO ENTIENDO POR IMPORTANTE, PERO NO RELEVANTE.
Desde el punto de vista del test, da igual cómo se llame el usuario o qué edad tenga. La aserción del test no cambiará si modificamos esos datos. Por eso, conviene eliminar este ruido del cuerpo del test, moverlo a otra clase y así reducir su complejidad y hacerlo más legible.
Para este tipo de situaciones, suelo crear una clase llamada TestData
, donde defino constantes e instancias necesarias para los tests cuyo contenido no afecta al resultado.
class TestData:
ANY_USER_ID = uuid4()
ANY_USER_NAME = "any-user-name"
ANY_USER_AGE = 42
ANY_USER_EMAIL = "any@email.com"
ANY_USER_CITY = "any-city"
ANY_USER_COUNTRY = "any-country"
@staticmethod
def a_user() -> User:
return User(
TestData.ANY_USER_ID,
TestData.ANY_USER_NAME,
TestData.ANY_USER_AGE
TestData.ANY_USER_EMAIL
TestData.ANY_USER_CITY
TestData.ANY_USER_COUNTRY
)
@staticmethod
def a_payload_from_user(user: User) -> dict:
return {
"id": user.user_id.hex,
"name": user.name,
"age": user.age,
"email": user.email,
"city": user.city,
"country": user.country
}
Al hacer uso de esta clase TestData
, mirad lo sencillo que queda nuestro test inicial:
class TestUsersRouter:
def test_create_user(self) -> None:
user = TestData.a_user()
payload = TestData.a_payload_from_user(user)
command = CreateUserCommand(user)
client = TestClient(app)
handler = Mimic(Spy, CreateUserCommandHandler)
app.dependency_overrides[_get_create_users_command_handler] = lambda: handler
response = client.post("/api/v1/users", json=payload)
expect(response.status_code).to(equal(CREATED))
expect(handler.execute).to(have_been_called_with(command))
Conclusiones
En resumen, deberíamos evitar incluir en nuestros tests información que sea importante pero no relevante.
Todo aquello que no afecte directamente al resultado del test debe trasladarse fuera del mismo. Así, conseguimos que nuestros tests sean lo más simples y breves posible, sin sacrificar su capacidad de ser autoexplicativos.
Por último, es importante no cometer el error contrario: no debemos mover fuera del test información que sí sea relevante para su comprensión o funcionamiento.