Los patrones de diseño nos ayudan a solventar situaciones y problemas comunes de una manera eficiente y estableciendo un lenguaje común, útil para que el código sea más fácil de entender. Es decir, como mucha gente en el pasado ha solucionado un problema similar al tuyo, reutilizamos una solución común sin tener que reinventar la rueda.
Hay dos soluciones muy habituales, y especialmente empleadas en la elaboración de tests: Builder Pattern y ObjectMother.
Ventajas de su uso en tests
Al hacer tests automáticos que requieran la creación de un objeto, nos puede surgir la necesidad de separar la creación del mismo del código del test en sí, especialmente para mejorar la legibilidad. Crear un objeto genera ruido y dificulta ver lo que realmente hace el test, y por ello se recurre a delegar esa tarea creacional, y ya de paso reducimos el acoplamiento (imagina que tienes cientos de tests que usan un objeto y tienes que modificar su constructor…).
Aunque tanto Builder Pattern como Object Mother son técnicas muy utilizadas en la elaboración de tests, no hay que olvidar que su uso no está limitado a ese ámbito, siendo también bastante habitual el uso de Builder Pattern en código de producción.
Builder Pattern
Un Builder nos permite construir un objeto y ponerlo en el estado que necesitemos, obviando los detalles que no son necesarios para el consumidor mediante la asignación de unos valores por defecto cualquiera.
Es decir, si tengo una clase Dog y estoy testando cálculos basados en su edad, es irrelevante cual sea el nombre del perro o su peso, con lo cual puedo utilizar valores por defecto para los datos que no me importan, y ni siquiera necesito saber qué valore son esos.
Además, la flexibilidad del Builder nos permite crear el objeto de manera incremental; podemos crear desde un Dog con solo valores por defecto, hasta uno con todos sus datos personalizados.
Es habitual para mejorar aún más la legibilidad y facilidad de uso del Builder, que éste se implemente con Fluent API. De esta manera, al utilizar un Builder encapsulamos la construcción del objeto y la forma de ponerlo en el estado requerido.
Ejemplo de Builder en Kotlin:
class DogBuilder {
private var age: Age = Age.ADULT
private var weight: Double = 10.0
private var isSterilized = false
fun withAge(age: Age): DogBuilder {
this.age = age
return this
}
fun withWeight(weight: Double): DogBuilder {
this.weight = weight
return this
}
fun isSterilized(isSterilized: Boolean): DogBuilder {
this.isSterilized = isSterilized
return this
}
fun build() : Dog {
return Dog("Logan",
age,
weight,
isSterilized)
}
}
Uso del Builder en un test:
@Test
fun calculate_10_percent_when_dog_is_a_puppy_of_2_months() {
val dog = DogBuilder()
.withAge(Age.PUPPY_2_MONTHS)
.build()
var diet = dog.calculateDiet()
assertEquals(10, diet.dailyFoodQuantity)
}
Como vemos en el ejemplo, no nos importa cómo se llame el perro, su peso, ni si está esterilizado, para el test solo es relevante la edad, porque es el valor a tener en cuenta para el cálculo realizado. De esta manera, quitamos el ruido que supondría utilizar el constructor de Dog y escribir todos sus parámetros, y nos centramos sólo en los que importan para el test.
ObjectMother
Es una clase que nos proporciona instancias “precocinadas” de las clases que usamos con cierta frecuencia, una especie de factoría de la combinación de valores que conforman un sujeto típico. Esta técnica es especialmente útil cuando existen combinaciones de datos concretas que se usan en muchos tests.
Por ejemplo, si tenemos una clase User y nos interesa interactuar con el país del usuario, es fácilmente reconocible que tengamos un ObjectMother que devuelva “John” (un usuario de USA) y otro usuario “Juan” (un usuario de España).
Al igual que un Builder, pero de manera más sencilla, un ObjectMother nos permite que los tests no dependan directamente de los constructores de los objetos, siendo especialmente útil para datos estáticos que no cambian (por ejemplo, los datos maestros como países, monedas, etc).
Sin embargo, en su sencillez reside su limitación, ya que no nos permite modificar los valores predefinidos del objeto creado, y no hay que caer en la tentación de ir creando más y más combinaciones de datos según las necesitemos creando un ObjectMother gigante; para eso es mejor usar un Builder que es más flexible.
Ejemplo de ObjectMother en Kotlin:
class DogObjectMother {
companion object {
fun puppy2Months() : Dog = Dog(
"Logan",
Age.PUPPY_2_MONTHS,
10.0,
false)
fun puppy4Months() : Dog = Dog(
"Sookie",
Age.PUPPY_4_MONTHS,
12.0,
false)
}
}
Uso del ObjectMother en un test:
@Test
fun calculate_10_percent_when_dog_is_a_puppy_of_2_months() {
val dog = DogObjectMother.puppy2Months()
val diet = dog.calculateDiet()
assertEquals(10, diet.dailyFoodQuantity)
}
En el ejemplo, podemos ver como estamos abstrayendo totalmente la creación del objeto y su asignación de valores, y por el propio método del ObjectMother sabemos cómo es el perro que estamos creando.
Conclusión
Para construir objetos en nuestros tests sin acoplar el código a sus constructores, y manteniéndolos legibles, podemos usar técnicas como Builder Pattern y ObjectMother.
Como cualquier patrón, es mejor esperar a que sea el propio código el que te indique cuál es el momento de implementarlos, ya que la aplicación de estas técnicas requiere código a escribir y mantener y no siempre compensa. En lugar de empezar a escribir Builders y ObjectMothers, empieza por opciones más simples y espera a que el código se exprese. Cuando aparezca código duplicado, acoplado o poco legible será el momento de refactorizar, y además podrás elegir con mejor criterio si te conviene más la sencillez del ObjectMother, o la flexibilidad del Builder.
Nota: Este texto fue publicado originalmente en el blog de Kirei Studio.