¿Alguna vez has tenido que cambiar multitud de tests después de añadir una propiedad en una clase? Si es así tengo malas noticias para ti, posiblemente tus tests no están bien hechos. ¡Por suerte hoy vamos a ver como solucionarlo!
Smell en los tests
Por simplicidad vamos a asumir que estamos trabajando en una arquitectura en capas con el patrón CQRS, usando implementaciones en lugar de interfaces. Si quieres pararte aquí y echarle un ojo a todos estos conceptos te dejo un repo de ejemplo.
Imaginemos por un momento que tenemos en nuestro dominio el concepto Student
representado por la siguiente clase:
from dataclasses import dataclass
@dataclass
class Student:
name: str
last_name: str
age: int
email: str
phone: str
address: str
zip_code: int
country: str
Imaginemos también que nos asignan una nueva tarea que consiste en implementar
la creación de Students
en nuestra app.
Para ello vamos a crear un CreateStudentCommandHandler
como caso de uso,
este caso de uso tendrá un colaborador de tipo StudentRepository
que será el encargado de hacer la creación de los estudiantes.
Como no puede ser de otra manera vamos a trabajar con TDD por lo que antes de nada vamos
a crear un test que nos permita replicar cómo sería la creación de un Student
en nuestro sistema:
from doublex import Mimic, Spy
from expects import have_been_called_with
...
class TestCreateCommandHandler:
def test_creates_a_student(self) -> None:
student = Student(
name = "any-name"
last_name = "any-last-name"
age = 1
email = "any-email"
phone = "+34123456789"
address = "any-address"
zip_code = 12345
country = "any-contry")
create_student_command = CreateStudentCommand(student)
student_repository = Mimic(Spy, StudentRepository)
create_student_command_handler = CreateStudentCommandHandler(student_repository)
create_student_command.process(create_student_command)
expect(student_repository.save).to(have_been_called_with(student))
¿Os llama la atención algo en este test? Para mí gusto tiene varios smells:
- Es muy grande, un test de más de 5-6 líneas no es buena señal.
- La parte del arrange es gigante (12 líneas del total de 14 del test).
- Hay muchos valores que no son relevantes (toda la parte de creación del
Student
) para el test generando una carga cognitiva mayor. - Es difícil saber a simple vista qué hace este test.
Ahora imaginemos que como este ejemplo tenemos decenas de tests con una estructura similar,
es decir creando una instancia de la clase Student
.
¿Qué ocurriría si por necesidades del dominio tenemos que añadir una propiedad a nuestra clase Student?
Pues imagino que ya me veréis venir, tendremos que actualizar todos los tests que creen un objeto Student
,
lo que para mí es un smell importante.
¡Así que vamos a solucionarlo!
Builder Pattern al rescate
Por suerte este smell tiene fácil solución usando el patrón Builder el cual se caracteriza principalmente por facilitar enormemente la creación de objetos.
Además vamos a lograr una mejora extra en nuestros tests ya que se van a simplificar muchísimo como veremos más adelante.
class StudentBuilder:
def __init__(self) -> None:
self._name = "any-name"
self._last_name = "any-last-name"
self._age = 1
self._email = "any-email"
self._phone = "+34123456789"
self._address = "any-address"
self._zip_code = 12345
self._country = "any-country"
def build(self) -> Student:
return Student(
name = self._name
last_name = self._last_name
age = self._age
email = self._email
phone = self._phone
address = self._address
zip_code = self._zip_code
country = self._country)
Más allá de las peculiaridades que puede tener crear un Builder
en Python el
patrón no tiene mucha historia. En su constructor asignamos unos valores por defecto
y en el método build
creamos el objeto Student
.
Una vez disponemos de nuestro Builder
vamos a actualizar nuestro test haciendo uso
de él.
from doublex import Mimic, Spy
from expects import have_been_called_with
...
class TestCreateCommandHandler:
def test_creates_a_student(self) -> None:
student = StudentBuilder().build()
create_student_command = CreateStudentCommand(student)
student_repository = Mimic(Spy, StudentRepository)
create_student_command_handler = CreateStudentCommandHandler(student_repository)
create_student_command.process(create_student_command)
expect(student_repository.save).to(have_been_called_with(student))
Aquí destacaría dos cosas:
- El test se ha reducido de 14 líneas a 6 (casi un 60%!) por el simple hecho de usar un
StudentBuilder
. - Ahora es muchísimo más fácil entender que hace el test a primera vista.
- Si en el futuro tenemos que añadir o quitar propiedades en la clase
Student
solo habrá que hacerlo en elStudentBuilder
y no de forma individual en cada test.
Nuestro StudentBuilder
se nos queda pequeño
¿Pero qué ocurre si las propiedades del objeto Student
son relevantes y necesitamos especificarlas en el test?
Bastará con actualizar nuestro StudentBuilder
para añadir métodos que nos permitan especificar qué campos
por defecto queremos customizar:
class StudentBuilder:
def __init__(self) -> None:
...
def with_country(self, country: str) -> "StudentBuilder":
self._country = country
return self
def build(self) -> Student:
...
Y actualizar nuestro test será tan sencillo como llamar al metodo .with_country
una vez creado nuestro builder:
from doublex import Mimic, Spy
from expects import have_been_called_with
...
class TestCreateCommandHandler:
def test_creates_a_student(self) -> None:
student = StudentBuilder().with_country("Spain").build()
create_student_command = CreateStudentCommand(student)
student_repository = Mimic(Spy, StudentRepository)
create_student_command_handler = CreateStudentCommandHandler(student_repository)
create_student_command.process(create_student_command)
expect(student_repository.save).to(have_been_called_with(student))
Conclusiones
Como hemos podido ver en este sencillo ejemplo, hacer uso de Builders
en nuestros
tests nos va a permitir reducir significativamente el tamaño de los mismos permitiéndonos centrarnos en lo
realmente importante.
Para mí sin duda es una práctica muy sencilla de implementar y que aportar un valor inmediato tanto a la calidad como en la legibilidad de nuestro código.
!Espero que os haya gustado, un saludo!