This post is also available in english.

Cuando escribimos nuestros tests generalmente utilizamos ejemplos reales del comportamiento deseado para que estos sean fáciles de entender.

@Test
fun `cart total is the sum of all added products`() {
    val shampoo = Product("Shampoo", Money("$50"))
    val soap = Product("Soap", Money("$5"))
    
    cart.add(shampoo)
    cart.add(soap)
    
    assertThat(cart.total).isEqualTo(Money("$55"))
}

Cuando los objetos son complejos, crear instancias válidas con datos distintos se vuelve un problema para la claridad (mezclamos detalles con las cosas importantes) y además es tedioso de escribir.

val alice = Customer("Alice", "Brown", ContactInfo(Address("123 Main Street", "New York", "NY 10030")))
val bob = Customer("Bob", "Davies", ContactInfo(Address("456 Silver Street", "Seattle", "WA 98126")))

Sería genial si tuviésemos una forma de poder contar con ejemplos válidos de forma fácil.

val alice = Customer("Alice", "Brown", contactInfos.one())
val bob = Customer("Bob", "Davies", contactInfos.one())

Necesitamos tener el mismo tipo de cuidados que tenemos con el código productivo en el código de nuestros tests. Ambos necesitan ser mantenidos, por ende debemos aplicar los mismos principios de diseño de software.

El código de los tests debe revelar intención, ser fácil de entender, no debería tener duplicación de conocimiento, etc.

A medida que el software crece, generalmente terminamos con objetos que nos asisten con la creación de ejemplos y de escenarios de tests. Escribir nuevos tests debe ser fácil, tenemos que ser ágiles cuando hacemos TDD.

En este post les voy a mostrar una pequeña clase utilitaria que nos permite obtener ejemplos para nuestros tests.

Ejemplos

Nos gustaría poder contar con ejemplos comunes de forma rápida y fácil. Por ejemplo:

// Esto nos va a dar un ejemplo válido de un email. Ej: "alice@gmail.com".
val email = emails.one()
// Este valor debería ser distinto del anterior. Ej: "bob@gmail.com"
val otherEmail = emails.one()

// Podemos tener ejemplos de distintos tipos 
val aDate = dates.one()
val aName = names.one()
val aPhone = phones.one()

// Incluyendo nuestros value objects
val aCountry = countries.one()
val anAddress = addresses.one()

Nuestra pequeña clase utilitaria debería ser fácil de usar. Debe ser fácil construir nuestros propios ejemplos.

Así es como se construyen ejemplos con nuestra clase utilitaria:

val firstnames = TestExamples("Alice", "Bob", "Charlie", "David", "Erin", "Franck", "Grace", "Heidi")
val lastnames = TestExamples("Brown", "Johnson", "McCartney", "Borg", "Davies")   

// Podemos construir ejemplos dinámicamente a partir de otros ejemplos
val fullnames = TestExamples(List(30) { names.one() + " " + lastnames.one() })

// También podemos utilizar nuestros propios value objects
val emails = TestExamples(listOf("alice@gmail.com", "bob@gmail.com").map { Email(it) })
val addresses = TestExamples(List(20) { 
    Address(streets.one(), cities.one(), countryCodes.one(), zipCodes.one()) 
})

La clase TestExamples

Finalmente nuestra pequeña clase utilitaria…

class TestExamples<T>(examples: List<T>) {
    private var examples = ThreadLocal<List<T>>().apply { set(examples) }
    private var current = -1

    constructor(vararg examples: T): this(examples.toList())

    fun one(): T {
        current++
        return examples.get()[current % examples.get().size]
    }
}

El método one devuelve un nuevo ejemplo cada vez. Si agotamos todos los posibles ejemplos simplemente comienza de nuevo.

Como pueden ver esta pequeña clase utilitaria nos puede proveer un gran valor a la hora de escribir nuestros tests.

Espero que encuentren esto tan útil como lo hago yo, nos vemos la próxima!