leanmind logo leanmind text logo

Blog

TDD Avanzado

Las herramientas que se necesitan para aplicar TDD en el mundo real en cualquier proyecto.

Mejorando nuestros tests con aserciones de dominio en AssertJ

Por Jorge Aguiar Martín

En los últimos proyectos de Java/Kotlin en los que he estado, he descubierto una librería de aserciones, AssertJ, que a mi personalmente me gusta mucho más en verbosidad/legibilidad que JUnit, pero hasta hace poco, no conocía las funcionalidades que veremos hoy y que a mi parecer, nos ayuda muchísimo a dar contexto y a que nuestros tests sean autoexplicativos.

Desde que empecé con esto del testing, hay una premisa que me ha preocupado siempre, y es el hecho de tratar que un test falle solamente por una razón. Esto a mi modo de ver, siempre ha sido algo complicado de conseguir, porque por norma general, las aserciones que hacemos son con respecto a datos concretos. Entonces, ¿qué hago con una condición de dominio que implica varios campos de un objeto?, ¿cómo de correcto es tener varias aserciones en un test?. Todas estas preguntas, me han hecho replantearme que las aserciones no deben estar relacionadas con datos directamente, sino con reglas de dominio, pero claro, es muy complicado hacer eso con un simple assertEquals.

Entonces, una vez introducido el problema, veremos qué herramientas nos brinda AssertJ para que nuestros tests hablen el mismo lenguaje de dominio cuando falle un test. Es decir, que en vez de que un test nos diga “Recibí X y esperaba Y” nos diga “Esta condición de dominio no se da”.

Empecemos…

Vamos a introducir nuestro objeto de dominio:

package dev.assertj

data class Person(
    val name: String,
    val age: Int,
)

Como vemos, estamos representando a una persona, y nuestro objetivo es comprobar si una persona es mayor o menor de edad. Vamos a ver cómo se verían nuestros tests:

@Test
fun `miriam should be an adult`() {
    val miriam = Person(
          name = "Miriam",
          age = 12,
    )
    assertThat(miriam.age)
        .isGreaterThanOrEqualTo(18)
}

En este caso, sabemos que Miriam no es una persona adulta, por lo tanto este test fallará, y cuando falle nos dirá lo siguiente:

Mensaje de test fallando: Se esperaba que 12, fuera mayor o igual a 18

Como vemos, este fallo de test sólo nos dice que un dato no es tal como esperábamos. Entonces, si queremos darle nombre a esta regla de dominio para que AssertJ nos enseñe un mensaje relevante, tenemos varias opciones, veamos cuales son:

Usando as() y describeAs()

Empezamos por las funciones as y describedAs:

@Test
fun `describedAs() will display both the passed and the normal assertion error message when failing`() {
  val miriam = Person(
        name = "Miriam",
        age = 12,
  )
  assertThat(miriam.age)
      .describedAs("is an adult")
              .isGreaterThanOrEqualTo(18)
}

Ahora, cuando nuestro test falle, no sólo aparecerá el mensaje propio de AssertJ, sino nuestra descripción:

Mensaje de test fallando: “Expect actual: 12, to be greater or equal to: 18”

Sobreescribiendo el mensaje de error

Vamos entonces un pasito más allá. Lo siguiente, será que ya no aparezca ese mensaje predefinido de la comparación, sino que sólo aparezca el mensaje correspondiente a nuestro dominio. Esto lo podemos hacer con las funciónes withFailMessage y overridingErrorMessage.

@Test
fun `withFailMessage() will display only the message passed if the assertion fail without the normal assertion message`() {
    val miriam = Person(
          name = "Miriam",
          age = 12,
    )
    assertThat(miriam.age)
        .withFailMessage("is not an adult")
        .isGreaterThanOrEqualTo(18)
}

Mensaje de test fallando: “is not an adult. java.lang.AssertionError: is not an adult”

Comparaciones complejas

Vale, hasta ahora todo va bien, pero… ¿qué pasa si la condición depende de más de un campo? Para esto tenemos la función matches, que nos permite tener varias comprobaciones con un mensaje personalizado.

@Test
fun `will display the message that you passed when the passed supplier returns false`() {
    val miriam = Person(
          name = "Miriam",
          age = 13,
    )
    assertThat(miriam).matches({
          it.age > 18
    }, "is an adult")
}

Mensaje de test fallando: “Expecting actual: Person(name=Miriam, age=13), to match ‘is an adult’ predicate

Como podemos ver en el mensaje de error, nos pone el objeto.toString() seguido de nuestro mensaje, indicando que no cumple con ese predicado que hemos especificado.

Evitando duplicidad

Ahora bien, imaginemos que aplicamos todo esto en nuestros tests. Ahora se presenta un nuevo problema, y es el hecho de que estas “condiciones/premisas” están desperdigadas por los tests, y muchas de ellas seguro estarán en varios sitios duplicadas, por lo que si esa condición de dominio cambia, tendremos que estar pendientes a aplicar ese cambio en todos los sitios que esté presente.

Para evitar esa duplicidad de comprobaciones, existe en AssertJ lo que denominan Condition, por lo que podemos detectar esas reglas de dominio, dejarlas reflejadas en un sólo sitio común y reutilizarlas en nuestros tests.

val anAdult = Condition<Person>({ it.age >= 18 }, "an adult")
val aChild = Condition<Person>({ it.age < 18 }, "a child")

@Test
fun `will display a domain condition message based on the condition passed`() {
    val miriam = Person(
         name = "Miriam",
         age = 13,
    )
    assertThat(miriam).`is`(anAdult)
}

Mensaje de test fallando: “Expecting actual: Person(name=Miriam, age=13), to be an adult

Como podemos ver, no hay gran diferencia entre el mensaje anterior y éste, pero la gran ventaja es que ahora, tenemos la condición focalizada y en un solo sitio.

Personalizando asersiones

Por último, tenemos la manera que para mi, sería la ideal: crearnos nuestras propias aserciones,, y así extender el comportamiento del assertThat que tiene la librería. Tendría un aspecto tal que así:

class PersonAssert private constructor(
    actual: Person
) : AbstractAssert<PersonAssert, Person>(actual, PersonAssert::class.java) {

    companion object {
        fun assertThat(actual: Person) = PersonAssert(actual)
    }

    fun isChild(): PersonAssert {
        isNotNull
        if (actual.age >= 18) {
            failWithMessage("Expected <%s> to be a child but was an adult", actual.name)
        }
        return this
    }

    fun isAdult(): PersonAssert {
        isNotNull
        if (actual.age < 18) {
            failWithMessage("Expected <%s> to be an adult but was a child", actual.name)
        }
        return this
    }
}

Tal como podríamos adivinar, no sólo especificamos como se llaman nuestras condiciones, sino que también definimos los mensajes de error, pudiendo especificar cualquier cosa que necesitemos.

@Test
fun `will display our own assertions and failure messages`() {
    val miriam = Person(
         name = "Miriam",
         age = 13,
    )
    assertThat(miriam).isAdult()
}

Mensaje de test fallando: “Expecting  to be an adult but was a child

Como vemos, el mensaje que nos aparece es el que nosotros hemos especificado. De esta manera, seremos nosotros quienes tengamos el control de qué mensaje ponemos y lo que consideremos relevante, para una regla de dominio concreta.

Conclusiones

A fin de cuentas, nuestros tests forman parte de ese código que entrega valor y por lo tanto, deben tener las mismas condiciones de mantenibilidad y legibilidad que cualquier código productivo. Es decir, tiene que ser viable el mantener estos tests y asersiones a lo largo del tiempo, y por lo tanto, no debemos tomar las asersiones personalizadas por ejemplo, como bala de plata. A fin de cuentas, es código que habrá que mantener en un futuro y que irá evolucionando junto con el proyecto.

Para finalizar, animo a cualquier persona que lea esto a que indague más en las funcionalidades de AssertJ. La curiosidad es lo que me ha hecho descubrir estas funcionalidades, que han conseguido darme una solución a la incertidumbre que yo tenía conmigo mismo, con respecto a mi forma de escribir tests y asersiones. Seguro que ustedes también encuentran cosas en la librería que les puede solucionar un problema o mejorar un caso concreto.

Publicado el 29/05/2023 por
Jorge image

Jorge Aguiar Martín

¿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?

Impulsamos el crecimiento profesional de tu equipo de developers