...
Vamos a ver cómo realizar tests unitarios (con jUnit) en nuestros proyectos Spring Boot, con ejemplos de algunas acciones básicas.
Importante: Las clases encargadas de lanzar los test deben crearse dentro de los paquetes src/test/java/ y no con los del código fuente.
Clases de tests
Las clases en las que situemos nuestros tests tendrás que ir precedidas de la anotación @SpringBootTest:
...
| Bloque de código |
|---|
|
@Test
void getExpedienteTest() {
} |
Properties para el test
Se pueden definir propiedades específicas para los tests. Para ello, crearemos un archivo application-test.properties y lo colocaremos en src/main/resources. Después, para que los tests utilicen las properties de ese archivo, habrá que añadir la siguiente anotación a las clases de los tests:
| Bloque de código |
|---|
|
@TestPropertySource( locations = "classpath:application-test.properties" ) |
De este modo se cogerán las properties que estén presentes en este archivo, pero si no están en este pero sí en el application.properties normal, se cogerán las del application.properties.
| Info |
|---|
|
No se deben subir claves a GitLab, por lo que dejaremos en blanco las properties de usuario y password de la base de datos (el test debe mockear servicios externos, por lo que no se debe llamar a base de datos). Además, debemos poner la property spring.jpa.hibernate.ddl-auto con el valor none, para que no se compruebe la conexión a base de datos cuando arranque la aplicación para hacer un test. |
En gitlab no se están subiendo los ficheros application.properties, por lo que si tenemos test que necesiten alguna configuración de este fichero deberemos crear una variable de tipo File llamada PROPIEDADES_TEST y en Value copiar y pegar el contenido del fichero que necesitamos
Image Added
Before: ejecutar código antes de los tests
Disponemos de las anotaciones @BeforeEach y @BeforeAll para indicar código que queremos que se ejecute antes de cada test, o antes de todos los tests, respectivamente. Por ejemplo, para indicar que queremos hacer un mock (los mocks se explican más adelante) de la propiedad que contiene el secret antes de los tests, podemos hacerlo así:
| Bloque de código |
|---|
|
@BeforeEach
void mocksecret() {
ReflectionTestUtils.setField( jwtTokenUtil, "secret", "secreto" );
} |
Comprobar valores
En nuestros tests, disponemos de diferentes formas de hacer aserciones. La primera de ellas, para comprobar valores, sería utilizando assertThat, que proporciona diferentes métodos para comprobar los valores (isNull, isEmpty, isEqualTo, etc.):
...
| Bloque de código |
|---|
import static org.junit.jupiter.api.Assertions.assertThrows;
assertThrows( TokenExpiredException.class, () -> jwtTokenUtil.getClaim( "Bearer " + tokenCaducado ) );
assertThrows( UnauthorizedException.class, () -> jwtTokenUtil.getClaim( "asdf" ) ); |
...
Mock
Un mock es una simulación de un objeto real; nos permite simular su comportamiento de forma aislada, eliminando dependencias. A efectos prácticos, un objeto mock será un objeto de la clase que indiquemos, cuyos métodos devolverán null por defecto. Para crear estos mocks y definir el comportamiento de los métodos que nos interesen, utilizamos Mockito. Vamos a ver cada uno de estos pasos, primero cómo crear un mock, y después cómo personalizar el comportamiento de los métodos.
Crear mocks
Mockito.mock() - Mock de un objeto
Con el método Mockito.mock() podemos crear un mock de un objeto de una clase o interfaz. Esto es, el mock se aplicará únicamente a ese objeto. Si es un bean que por ejemplo utilice otra clase, este mock no se aplicará a ese bean, únicamente al objeto que hemos indicado. Lo haremos así, dentro de un test:
| Bloque de código |
|---|
|
UserRepository localMockRepository = Mockito.mock(UserRepository.class); |
@Mock - Mock de un objeto (propiedad de la clase de test)
Podemos indicar en las propiedades de nuestra clase que contiene los tests, que estas sean mocks. Esto es, al igual que en el caso anterior, que sólo se apliquen a estos objetos. Lo haremos con la anotación @Mock:
| Bloque de código |
|---|
|
@Mock
UserRepository mockRepository; |
@MockBean - Mock de un bean a nivel global
Por último, podemos añadir la anotación @MockBean las propiedades de la clase, que creará el mock a nivel global, de contexto. Esto quiere decir que todas las instancias de ese bean serán ese mock, en lugar de únicamente el objeto que definimos en la clase de los tests. Esto se aplicará a todos los tests que tengamos en la clase.
| Bloque de código |
|---|
|
@MockBean
UserRepository mockRepository; |
Mockear una propiedad de un objeto
Podemos mockear una propiedad de un objeto como hemos visto en un ejemplo anterior, utilizando ReflectionTestUtils.setField, indicando el objeto, el nombre de la propiedad, y el valor a asignarle:
| Bloque de código |
|---|
|
@Autowired
JwtTokenUtil jwtTokenUtil;
@BeforeEach
void mocksecret() {
ReflectionTestUtils.setField( jwtTokenUtil, "secret", "secreto" );
} |
Mockear un método
Para hacer un mock de un métidométodo, disponemos de Mockito.when(...), al que le indicamos el método, con sus argumentos, y que posteriormente nos ofrece varios métodos para la respuesta, como .thenReturn( ... ) para indicar el valor a devolver o .thenThrow( ... ) para indicar una excepción a lanzar. En el siguiente ejemplo podemos ver dos ejemplos de esto para hacer un mock de getClaims, creando un token previamente, pasando un valor concreto como argumento o indicando que cualquier String:
| Bloque de código |
|---|
|
import static org.mockito.ArgumentMatchers.any;
...
Map<String, Object> infoUser = new HashMap<>();
infoUser.put( "roleName", "G_OTROS" );
final Instant now = Instant.now();
// Creamos token con el usuario "guillermo.castillo@ticarum.es", codificado con el secret "secreto"
String token = Jwts.builder().setClaims( infoUser ).setSubject( "guillermo.castillo@ticarum.es" )
.setIssuedAt( Date.from( now ) ).setExpiration( Date.from( now.plus( Duration.ofDays( 123 ) ) ) )
.signWith( SignatureAlgorithm.HS256, TextCodec.BASE64.encode( "secreto" ) ).compact();
final Claims claims = Jwts.parser().setSigningKey( TextCodec.BASE64.encode( "secreto" ) ).parseClaimsJws( token ).getBody();
// Mock getClaims
Mockito.when( jwtTokenUtil.getClaim( "asdf" ) ).thenReturn(claims);
// Mock getClaims con cualquier argumento String
Mockito.when( jwtTokenUtil.getClaim( any(String.class) ).thenReturn(claims); |
Aquí vemos que en el último when, el argumento que se le pasa a getClaim es any(String.class), indicando que cuando se ejecute ese método con cualquier argumento que sea de tipo String, se devolverá lo que se indica. Al igual que any, podemos utilizar nullable, para indicar que puede ser o de la clase que indiquemos, o null. Para esto tendremos que añadir sus imports, que al ser static no nos los autocompletará Eclipse:
| Bloque de código |
|---|
|
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.nullable; |
Spy
A parte de hacer mocks, tenemos la opción de utilizar Spy, que en vez de crear un objeto falso, lo que nos permite es interceptar partes del objeto original. Se puede ver como un mock parcial, para por ejemplo simular el comportamiento de algunos métodos, pero manteniendo el comportamiento por defecto del resto. La forma de hacer esto es muy similar a los mocks, pero para simular el comportamiento de las llamadas a los métodos cambia un poco la sintaxis. Vamos a verlo.
Crear spies
Mockito.spy() - Spy de un objeto
Con el método Mockito.spy() podemos crear un spy de un objeto de una clase o interfaz. Esto es, el spy se aplicará únicamente a ese objeto. Si es un bean que por ejemplo utilice otra clase, este spy no se aplicará a ese bean, únicamente al objeto que hemos indicado. Lo haremos así, dentro de un test:
| Bloque de código |
|---|
|
UserRepository localMockRepository = Mockito.spy(UserRepository.class); |
@Spy - Spy de un objeto (propiedad de la clase de test)
Podemos indicar en las propiedades de nuestra clase que contiene los tests, que estas sean spies. Esto es, al igual que en el caso anterior, que sólo se apliquen a estos objetos. Lo haremos con la anotación @Spy:
| Bloque de código |
|---|
|
@Spy
UserRepository mockRepository; |
@SpyBean - Spy de un bean a nivel global
Por último, podemos añadir la anotación @SpyBean las propiedades de la clase, que creará el spy a nivel global, de contexto. Esto quiere decir que todas las instancias de ese bean serán ese spy, en lugar de únicamente el objeto que definimos en la clase de los tests. Esto se aplicará a todos los tests que tengamos en la clase.
| Bloque de código |
|---|
|
@SpyBean
UserRepository mockRepository; |
Simular un método con spy
En este caso, el concepto es igual que en los mocks, pero la sintaxis cambia ligeramente. En vez de Mockito.when( ... ).thenReturn( ... ), lo haremos con Mockito.doReturn( ... ).when( objeto ).metodo( ... ):
| Bloque de código |
|---|
|
Mockito.doReturn( "asdf" ).when( expedientesService ).getExpediente( nullable( String.class ) ); |
En el caso de que no queramos devolver nada, que sea un método void, en lugar de usar doReturn(...) utilizaremos doNothing().
Testear métodos @Transactional
Cuando se llama a un método @Transactional, se intentará abrir una conexión con base de datos al llamarlo, por lo que nos dará error directamente aunque hayamos mockeado las operaciones que se hagan dentro de dicho método. Para evitar esto, debemos añadir dos clases de configuración para gestionar las transacciones en los tests. Bajo una carpeta config, añadiremos estas dos clases:
| Bloque de código |
|---|
| language | java |
|---|
| title | NoOpTransactionManager.java |
|---|
|
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;
public class NoOpTransactionManager extends AbstractPlatformTransactionManager {
@Override
protected Object doGetTransaction() throws TransactionException {
return new Object();
}
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException {
}
@Override
protected void doCommit(DefaultTransactionStatus status) throws TransactionException {
}
@Override
protected void doRollback(DefaultTransactionStatus status) throws TransactionException {
}
} |
| Bloque de código |
|---|
| language | java |
|---|
| title | TestConfig.java |
|---|
|
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@TestConfiguration
@EnableTransactionManagement
public class TestConfig {
@Bean
public PlatformTransactionManager transactionManager() {
// Return a mock transaction manager or a do-nothing implementation
return new NoOpTransactionManager();
}
} |
Por último, en la clase de Test en la que se pruebe el método @Transactional, añadimos la anotación @Import(TestConfig.class):
| Bloque de código |
|---|
| language | java |
|---|
| title | Clase del test correspondiente |
|---|
|
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
@Import(TestConfig.class)
class TestSolicitudes {
...
...
...
} |
Simular peticiones a servicio REST
Para simular peticiones http a nuestros servicios REST podemos hacer uso de mockMvc, de este modo:
...
Este es un ejemplo muy simple, pero podemos ver que se realiza una llamada get (habiendo añadido el import static), y se espera que la respuesta sea un 200 OK.
Documentación