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
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…