[Número 002] Aserciones, Aserciones, Aserciones

Resumen: La ejecución de aserciones sobre valores en código de pruebas es una ocurrencia común en Java. Sin embargo, elegir el API adecuada y encontrar la cantidad de la información necesaria que se mostrará cuando una asserción falla puede ser difícil. Vamos a discutir algunas de las opciones que podemos encontrar en el ecosistema Java para lograr estos objetivos.

¡Bienvenidos a la edición número 2! Esta edición fué escrita mientras me encontraba en tránsito entre JBCNConf y Devoxx Polonia. La inspiración surgió a partir de una pregunta que me hicieron después de presentar Java libraries you can’t afford to miss (liga al video de una versión anterior presentada en JFokus 2017 en caso de que estés interesado), específicamente si había mencionado AssertJ como parte del conjunto de proyectos mencionaods. Resulta que no lo hice porque esta plática en particular cubre alrededor de 20 proyectos para código de producción y de pruebas, sin embargo existe una plática como seguimiento al tema de pruebas Testing Java code effectively donde si se discuten varios projectos relacionados con aserciones en las pruebas..

La mayoría de los desarrolladores Java están muy familiarizados con JUnit y Hamcrest, es muy probable que hayas escrito código de prueba con estos proyectos. Hamcrest facilita la definición de aserciones; cuando alguna de estas afirmaciones falla, se genera un mensaje de error que resulta ser más descriptivo que el uso de aserciones regulares proveídas por JUnit. Veamos el siguiente código de prueba

package com.andresalmiray.newsletter002;

import org.junit.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.describedAs;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.isEmptyString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;

public class HamcrestStringTest {
    @Test
    public void testsOnString() {
        String subject = "testing";
        assertThat(subject, describedAs("subject is not null", notNullValue()));
        assertThat(subject, describedAs("subject is not empty", not(isEmptyString())));
        assertThat(subject, describedAs("subject contains 'ing'", containsString("ing")));
        assertThat(subject, describedAs("subject = 'testing'", equalTo("testing")));
    }
}

Tenemos un String definido como sujeto de prueba y 4 aserciones aplicadas sobre el mismo. Observa la composición de la condición describeAs con otras condiciones (o matchers), dicha composición nos permite definir información adicional utilizada en el mensaje de error en caso de una falla. Observa también que el comportamiento de cualquier condicion puede invertirse al ser compuesto con el matcher not (), como lo hacemos con la aserción de contenido vacío. A pesar de que el código resulta ser bastante legible algunos personas probablemente prefieran una manera más descriptiva de leer el código, tal vey una manera más fluida. También existe el problema de elegir el macher adecuado, ya que hay muchos subtipos y fábricas de matchers para elegir, esto significa que probablemente tengas que hacer un poco de investigación con el fin de escribir pruebas más concisas. Finalmente, existe el problema de tener demasiadas aserciones dentro de una sola prueba. ¿Qué sucede si falla la tercera aserción? La cuarta no se ejecutará. Estos temas son tratados por AssertJ, Truth, y JGoTesting.

Tanto AssertJ como Truth pueden rastrear sus orígenes a Fest-assert, otro proyecto de aserciones que introdujo un API des diseño fluído junto con un mecanismo de extensión. AssertJ y Truth tomaron estas ideas y añadieron más características. Para empezar, podemos obtener un matcher apropiado para un sujeto en particular invocando las capacidades de sugerencia de código en tu IDE favorito. En el caso de AssertJ obtienes acceso a más condiciones en tipos de base, como la verificación de blank en una cadena de caracteres, también no es necesario negar la aserción de vacío, ya que existe una condición como esa! El siguiente código demuestra como podemos reescribir la prueba anterior usando AssertJ

package com.andresalmiray.newsletter002;

import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class AssertJStringTest {
    @Test
    public void testsOnString() {
        String subject = "testing";
        assertThat(subject).as("subject is not null").isNotNull();
        assertThat(subject).as("subject is not null").isNotEmpty();
        assertThat(subject).as("subject is not blank").isNotBlank();
        assertThat(subject).as("subject contains 'ing'").contains("ing");
        assertThat(subject).as("subject has size = 7").hasSize(7);
        assertThat(subject).as("subject = 'testing'").isEqualTo("testing");
    }
}

Observa el flujo de las aserciones, tal vez te parezca una forma de lectura mas adecuada con tus preferencias. Además, se requieren menos declaraciones de importación, ya que los aserciones fluyen desde la aserción inicial, siguiendo el tipo del sujeto (Stirng en este caso). Ahora bien, si alguna de estas aserciones falla, el resto no será ejecutado, al igual como sucede con JUnit y Hamcrest; ¿Cómo solucionamos este problema? Fácil, AssertJ utiliza una característica que se encuentra en JUnit llamado reglas, que le permiten decorar los métodos de prueba con comportamiento adicional ejecutado antes y después de cada prueba. El código anterior se puede volver a escribir como sigue

package com.andresalmiray.newsletter002;

import org.assertj.core.api.JUnitSoftAssertions;
import org.junit.Rule;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class AssertJStringTest {
    @Rule
    public final JUnitSoftAssertions softly = new JUnitSoftAssertions();

    @Test
    public void fluentAllChecksOnString() {
        String subject = "something";
        softly.assertThat(subject).as("subject is not null").isNotNull()
            .as("subject is not null").isNotEmpty()
            .as("subject is not blank").isNotBlank()
            .as("subject contains 'ung'").contains("ung")
            .as("subject has size = 7").hasSize(7)
            .as("subject = 'testing'").isEqualTo("testing");
    }
}

Incluso conseguimos acortar el código siguiendo el API fluída, añadiendo todos los aserciones una detrás de la otra. La regla JUnitSoftAssertions capturará todos las fallas durante la ejecución del método de prueba y imprimirá todos los errores que hayan ocurrido al finalizar dicha prueba. Como mencioné antes, Truth sigue un enfoque similar, aunque no proporciona tantas aserciones base como AssertJ. El código anterior reescrito con Truth resulta en lo siguiente

package com.andresalmiray.newsletter002;

import com.google.common.truth.Expect;
import org.junit.Rule;
import org.junit.Test;

import static com.google.common.truth.Truth.assertThat;


public class TruthStringTest {
    @Test
    public void testsOnString() {
        String subject = "testing";
        assertThat(subject).named("subject is not null").isNotNull();
        assertThat(subject).named("subject is not null").isNotEmpty();
        assertThat(subject).named("subject contains 'ing'").contains("ing");
        assertThat(subject).named("subject has size = 7").hasLength(7);
        assertThat(subject).named("subject = 'testing'").isEqualTo("testing");
    }

    @Rule
    public final Expect expect = Expect.create();

    @Test
    public void allChecksOnString() {
        String subject = "something";
        expect.that(subject).named("subject").isNotNull();
        expect.that(subject).named("subject").isNotEmpty();
        expect.that(subject).named("subject").contains("ung");
        expect.that(subject).named("subject").hasLength(7);
        expect.that(subject).named("subject").isEqualTo("testing");
    }
}

Perdimos la capacidad de comprobar si una cadena puede o no contener espacios, en particular las aserciones efectuadas en cadenas de caratcteres no permiten el encadenamiento adicional utilizando la API fluída, pero si pueden aplicarse a otros tipos, como colecciones. Vale la pena notar que tanto AssertJ como Truth te permiten crear tus propios matchers que pueden ser adaptados a la medida para tipos que son específicos a tu proyecto. Finalmente, JGoTesting, un proyecto inspirado en las capacidades de prueba ofrecidas por el lenguaje de programación Go. JGoTesting se puede utilizar para comprobar cualquier cantidad de condiciones booleans y matchers de Hamcrest. El siguiente código demuestra cómo podríamos volver a escribir las pruebas anteriores

package com.andresalmiray.newsletter002;

import org.jgotesting.rule.JGoTestRule;
import org.junit.Rule;
import org.junit.Test;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.describedAs;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.isEmptyString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;

public class JGoTestingStringTest {
    @Rule
    public final JGoTestRule t = new JGoTestRule();

    @Test
    public void testsOnString() {
        String subject = "testing";
        t.check(subject, describedAs("subject is not null", notNullValue()));
        t.check(subject, describedAs("subject is not empty", not(isEmptyString())));
        t.check(subject, describedAs("subject contains 'ing'", containsString("ing")));
        t.check(subject, describedAs("subject = 'testing'", equalTo("testing")));
    }

    @Test
    public void fluentAllChecksOnString() {
        String subject = "something";
        t.check(subject, describedAs("subject is not null", notNullValue()))
            .check(subject, describedAs("subject is not empty", not(isEmptyString())))
            .check(subject, describedAs("subject contains 'ung'", containsString("ung")))
            .check(subject, describedAs("subject = 'testing'", equalTo("testing")));
    }
}

Utilizado de esta manera, JGoTesting puede proporcionar el mismo comportamiento de aserciones suaves de AssertJ, pero para Hamcrest matchers. Esto es tan solo la punta del iceberg, todos los proyectos aquí vistos ofrecen más características de lo que se ha demostrado hasta ahora. Espero haber despertado tu curiosidad para darle una mirada mas a fondo a estos proyectos. Todo el código demostrado aquí esta disponible en GitHub.

Gracias por tu tiempo. Cualquier comentario es apreciado.

Nos vemos la próxima vez.

Andrés

ˆ Back To Top