1. Introducción

Hola de nuevo a todos y bienvenidos a un nuevo post de nuestro querido blog!. En esta ocasión hablaremos de los Rules de Junit. En numerosas ocasiones nos vemos en la obligación de repetir una cierta operativa en todos los test de una clase. Por tanto, parece una buena idea disponer de un mecanismo que nos permitiese ejecutarla antes o después de cada test. Es aquí cuando se hace necesaria la aparición de los Rules de Junit!. Pero, ¿en qué se diferencia de las anotaciones @After, @Before, @AfterClass o @BeforeClass?. Pues bien, viene a ser lo mismo pero con un poco más de flexibilidad. Además, Junit nos provee de algunas implementaciones que nos ayudarán en la realización y ejecución de nuestros test.

Os podéis descargar el código de ejemplo de mi GitHub aquí.

Tecnologías empleadas:

  • Java 8
  • Gradle 3.1
  • Junit 4.12
  • Logback 1.2.3

2. Algunos de los Rules proporcionados por Junit

A continuación se muestras algunas de las implementaciones más importantes que nos provee Junit.

1. Timeout

Hasta ahora, si quería que la ejecución de los métodos de mi test no durase más de un determinado tiempo, hacía uso del atributo timeout de la anotación @Test

public class MyTest {

    @Test(timeout = 100)
    public void test1() throws IOException {
       ...
    }

    @Test(timeout = 100)
    public void test2() throws IOException {
       ...
    }

    @Test(timeout = 100)
    public void test3() throws IOException {
       ...
    }

    @Test(timeout = 100)
    public void test4() throws IOException {
       ...
    }
}

Sin embargo podemos utilizar el Rule Timeout que nos proporciona Junit para hacer exactamente lo mismo.

public class MyTest {

    @Rule
    public Timeout timeout = Timeout.builder().withTimeout(100L, TimeUnit.MILLISECONDS).build();

    public void test1() throws IOException {
       ...
    }

    public void test2() throws IOException {
       ...
    }

    public void test3() throws IOException {
       ...
    }

    public void test4() throws IOException {
       ...
    }
}

2. TemporaryFolder

En caso de necesitar un directorio temporal en nuestros métodos podemos hacer uso de la implementación TemporaryFolder

package com.jorgehernandezramirez.unit.rules;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class TemporaryFolderRuleTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(TemporaryFolderRuleTest.class);

    @Rule
    public TemporaryFolder temporaryFolder = new TemporaryFolder();

    @Test
    public void shouldCreateNewFileOverTemporaryFolder() throws IOException {
        final File file = temporaryFolder.newFile("spring.png");
        assertTrue(file.exists());
        assertEquals(temporaryFolder.getRoot(), file.getParentFile());
        LOGGER.info("{}", temporaryFolder.getRoot());
    }
}

3. Exception

Hasta ahora para esperar el lanzamiento de una excepción en un test utilizaba el atributo excepted en la anotación @Test

@Test(expected = IllegalArgumentException.class)
public void test1(){
   ...
}

Sin embargo, la implementación ExpectedException es el rule que nos proporciona Junit que nos permite además de esperar el lanzamiento de una excepción en un determinado test, determinar cual va a ser el mensaje o la causa del mismo.

package com.jorgehernandezramirez.unit.rules;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

import static org.hamcrest.core.Is.isA;
import static org.junit.Assert.assertTrue;

public class NoExceptionExceptionRuleTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(NoExceptionExceptionRuleTest.class);

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Test
    public void shouldEndWithoutAnyException() throws IOException {
        assertTrue(true);
    }

    @Test
    public void shouldEndWithAnException() throws IOException {
        expectedException.expect(UnsupportedOperationException.class);
        expectedException.expectMessage("This is an unsupported operation");
        expectedException.expectCause(isA(RuntimeException.class));
        expectedException.reportMissingExceptionWithMessage("This is a");
        throw new UnsupportedOperationException("This is an unsupported operation", new RuntimeException());
    }
}

3. Custom Rules

Y, ¿podemos crear nuestros propios Rules y utilizarlos en nuestras clases? Sí!, basta con implementar la interfaz TestRule y ya podríamos empezar a hacer uso de ellos.

A continuación se muestra nuestra implementación de un Rule de Junit. Su único cometido es imprimir una traza antes y después de la ejecución de cada test de nuestra clase.

package com.jorgehernandezramirez.unit.rules.custom;

import com.jorgehernandezramirez.unit.rules.custom.annotation.CustomAnnotation;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CustomLogTestRule implements TestRule {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomLogTestRule.class);

    private static final String EMPTY = "";

    private CustomLogTestRule(){
        super();
    }

    public static CustomLogTestRule build(){
        return new CustomLogTestRule();
    }

    @Override
    public Statement apply(final Statement base, final Description description) {
        return new CustomLogStatement(base, description);
    }

    private class CustomLogStatement extends Statement{

        private final Statement base;

        private final Description description;

        public CustomLogStatement(final Statement base, final Description description){
            this.base = base;
            this.description = description;
        }

        @Override
        public void evaluate() throws Throwable {
            LOGGER.info("Init test -> {}, {}, {}", description.getMethodName(),
                    description.getDisplayName(), getCustomAnnotationValue(description));
            base.evaluate();
            LOGGER.info("End test -> {}, {}, {}", description.getMethodName(),
                    description.getDisplayName(), getCustomAnnotationValue(description));
        }
    }

    private String getCustomAnnotationValue(final Description description){
        final CustomAnnotation customAnnotation = description.getAnnotation(CustomAnnotation.class);
        if(customAnnotation == null){
            return EMPTY;
        }
        return customAnnotation.value();
    }
}

La implementación anterior imprime además el parámetros value de la anotación @CustomAnnotation que podemos utilizar en nuestros tests.

package com.jorgehernandezramirez.unit.rules.custom.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface CustomAnnotation {

    String value();
}

Finalmente nuestro test.

package com.jorgehernandezramirez.unit.rules;

import com.jorgehernandezramirez.unit.rules.custom.CustomLogTestRule;
import com.jorgehernandezramirez.unit.rules.custom.annotation.CustomAnnotation;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class CustomLogRuleTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomLogRuleTest.class);

    @Rule
    public CustomLogTestRule timeout = CustomLogTestRule.build();

    @CustomAnnotation("test 1")
    @Test
    public void shouldBeTrue() {
        LOGGER.info("Executing test 1...");
        assertTrue(true);
    }

    @CustomAnnotation("test 2")
    @Test
    public void shouldBeFalse() {
        LOGGER.info("Executing test 2...");
        assertFalse(false);
    }
}

La ejecución del mismo da como resultado

09-06-2017 19:37:07 [main] INFO c.j.unit.rules.custom.CustomLogTestRule – Init test -> shouldBeTrue, shouldBeTrue(com.jorgehernandezramirez.unit.rules.CustomLogRuleTest), test 1
09-06-2017 19:37:07 [main] INFO c.j.unit.rules.CustomLogRuleTest – Executing test 1…
09-06-2017 19:37:07 [main] INFO c.j.unit.rules.custom.CustomLogTestRule – End test -> shouldBeTrue, shouldBeTrue(com.jorgehernandezramirez.unit.rules.CustomLogRuleTest), test 1
09-06-2017 19:37:07 [main] INFO c.j.unit.rules.custom.CustomLogTestRule – Init test -> shouldBeFalse, shouldBeFalse(com.jorgehernandezramirez.unit.rules.CustomLogRuleTest), test 2
09-06-2017 19:37:07 [main] INFO c.j.unit.rules.CustomLogRuleTest – Executing test 2…
09-06-2017 19:37:07 [main] INFO c.j.unit.rules.custom.CustomLogTestRule – End test -> shouldBeFalse, shouldBeFalse(com.jorgehernandezramirez.unit.rules.CustomLogRuleTest), test 2

4. Class Rule

Para poder crear un Rule a nivel de clase que se pueda ejecutar antes y después de todos los test de una clase, debemos crear una implementación de la clase ExternalResource

package com.jorgehernandezramirez.unit.rules.custom;

import org.junit.rules.ExternalResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyServer extends ExternalResource {

    private static final Logger LOGGER = LoggerFactory.getLogger(MyServer.class);

    @Override
    protected void before() throws Throwable {
        LOGGER.info("Starting the server...");
    }

    @Override
    protected void after() {
        LOGGER.info("Shutdown the server...");
    }
}

Para inyectarlo dentro de nuestro test deberemos hacer uso de la anotación @ClassRule sobre un atributo estático.

package com.jorgehernandezramirez.unit.rules;

import com.jorgehernandezramirez.unit.rules.custom.MyServer;
import com.jorgehernandezramirez.unit.rules.custom.annotation.CustomAnnotation;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class MyServerClassRuleTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(MyServerClassRuleTest.class);

    @ClassRule
    public static MyServer server = new MyServer();

    @Test
    public void shouldBeTrue() {
        LOGGER.info("Executing test 1...");
        assertTrue(true);
    }

    @Test
    public void shouldBeFalse() {
        LOGGER.info("Executing test 2...");
        assertFalse(false);
    }
}

Tras la ejecución del test obtenemos por consola

09-06-2017 19:41:57 [main] INFO c.j.unit.rules.custom.MyServer – Starting the server…
09-06-2017 19:41:57 [main] INFO c.j.unit.rules.MyServerClassRuleTest – Executing test 1…
09-06-2017 19:41:57 [main] INFO c.j.unit.rules.MyServerClassRuleTest – Executing test 2…
09-06-2017 19:41:57 [main] INFO c.j.unit.rules.custom.MyServer – Shutdown the server…