8 jun 2020

TDD Spring Boot (I)

Contenido

Parte I

  • Proyecto Maven SPBoot
  • Configuracion POM.XML
    • Mockito
    • JaCoCo
  • Elegir Junit4 VS Junit5
  • Simple Example
    • TDD Controller
    • TDD Service
    • TDD Repository
    • TDD Controller segunda aproximación

Parte II

  • Junit5 en profundidad
  • Diferentes anotaciones para TDD en SpringBoot
    • @SpringBootTest - SpringBootTest loads complete application and injects all the beans which is can be slow
    • @WebMvcTest - for testing the controller layer
    • @JsonTest - for testing the JSON marshalling and unmarshalling
    • @DataJpaTest - for testing the repository layer
    • @RestClientTests - for testing REST clients
  • Mockito en profundidad
  • References

Proyecto Maven SPBoot

Creamos un proyecto nuevo y nos aseguramos que tenga la dependencia:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>

Por defecto ya la incluye.

Configuraciones POM.XML

Vamos a necesitar 2 herramientas para poder realizar los TESTs, la primera es para generar los mocks y se llama Mockito, y la segunda es para medir el coverage o cobertura, y es JaCoCo.

JaCoCo

<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.7.201606060606</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
La configuracion más importante es decirle que haga los tests y al mismo tiempo genere el informe de coverage:
<phase>test</phase>
Se encargaría de esto.
Cuando ejecutemos
$ mvn test
tendremos en la carpeta:
target/site/jacoco/index.html
El archivo con el informe de cobertura.

Mockito

Es el framework que usaremos para realizar los mocks necesarios.
* Al incorporar las dependencias de Junit5 en SpringBoot deberíamos tenerlas incorporadas. (revisar este punto)

Elegir Junit4 VS Junit5

Aunque Junit5 ya lleva varios años, la mayor parte de la documentación está para Junit4.
Las pples cosas a tener en cuenta para aplicar Junit5
POM.XML
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
Añadimos las dependencias de Jupiter (Junit5 para diferenciarlo de Junit4)
Debemos EXCLUIR la versión anterior que viene con Spring de Junit4.

Los tests también se arrancan de otra forma, las pples diferencias a tener en cuenta serían:
  1. La anotación @RunWith al Test ahora es reemplazada por @ExtendWith, y hay una nueva clase de soporte para Spring.
  2. Las anotaciones @Test ahora están en un paquete diferente.
  3. Los asserts ahora están en otro paquete, y usan un api diferente.
  4. Las excepciones ahora se tratan directamente dentro del cuerpo del método de Test (y no con un atributo en la anotación).
  5. Las anotaciones @Before y @After ahora son @BeforeEach y @AfterEach

Esta sería la forma de arrancar un Test con Junit5
@SpringBootTest
@ExtendWith(SpringExtension.class)

Simple Example

¿Como sería una programación TDD con SpringBoot?
El enfoque es sencillo, deberíamos empezar con los Tests. Para ello ¿Cómo realizaríamos nuestro planteamiento? Imaginemos que tenemos el siguiente esquema

CONTROLLER

Lo primero es generar el ENDPOINT para lo cual vamos a crear nuestro Test que permita tener ese ENDPOINT.
@WebMvcTest
@ExtendWith(SpringExtension.class)
public class HelloControllerTest {
}

Como vemos usamos @WebMvcTest y necesitamos dos cosas más, algo que nos permita hacer la llamada al endpoint y un MOCK que nos responda como el Service de la siguiente capa. Como queremos hacer test unitarios y que cada capa se testee de manera independiente, generamos ese mock.   
public class HelloControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
private HelloService helloSvc;

Llegados a este punto vemos que no compila. Lógicamente nos falta ese HelloService   que estamos inyectando.
Vamos a crearlo:

@Service
public class HelloServiceImpl implements HelloService {
}

Ahora ya sí, no hay problema de compilación.

Nuestro ENDPOINT solo debe saludarnos, por lo que vamos a ello:
@Test
void getHello() throws Exception {
when(helloSvc.get()).thenReturn("Hello JUnit 5");
mockMvc.perform(MockMvcRequestBuilders.get("/hello").contentType(MediaType.ALL))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("Hello JUnit 5"));
}

Pero no compila, vemos el código:
helloSvc.get()
Ese método no existe, así necesitamos crearlo.
@Override
public String get() {
return "";
}

Ahora sí, ya compila, pero el Test ha fallado. O quizá es eso lo que debiera hacer para ahora empezar a generar código poco a poco y que vaya siendo testeable.

SERVICE

Vamos a la siguiente capa.
Queremos testear el servicio, que debería traerse de la Base de Datos un objeto, en nuestro caso por simplicidad hemos optado por un String, y posteriormente devolverlo.
Vamos a ello
@Service
public class HelloServiceImpl implements HelloService {

@Autowired
HelloRepository helloRepository;

@Override
public String get() {
return helloRepository.get();
}

}


Otra vez error de compilación. Vemos el código y se queja de que no tenemos HelloRepository.

Por tanto vamos a crearlo
@Repository
public class HelloRepositoryImpl implements HelloRepository {
@Override
public String get() {
return "Hello JUnit 5";
}
}


Con esto ya tenemos las piezas necesarias, vemos que según vamos haciendo el Test nos va solicitando lo que necesitamos, y por tanto vamos haciendo un código más robusto.

Service Test

@SpringBootTest
public class HelloServiceTest {
@Mock
private HelloRepository helloRepository;

@InjectMocks // auto inject helloRepository
private HelloService helloService = new HelloServiceImpl();

@BeforeEach
void setMockOutput() {
when(helloRepository.get()).thenReturn("Hello Mockito From Responsitory");
}

@DisplayName("Test Mock helloService + helloRepository")
@Test
void testGet() {
assertEquals("Hello Mockito From Responsitory", helloService.get());
}
}
Analicemos el código:
@Mock
private HelloRepository helloRepository;

Esta parte ya la entendemos, estamos creando un MOCK del Repository para evitar la llamada al mismo y poder testear cada capa.
Pero de nada sirve si en la siguiente capa que lo necesita no se lo inyectamos, ¿como?
@InjectMocks // auto inject helloRepository
private HelloService helloService = new HelloServiceImpl();

Por ultimo para cada petición vamos a simular la respuesta:
    @BeforeEach
    void setMockOutput() {
        when(helloRepository.get()).thenReturn("Hello Mockito From Responsitory");
    }

Así pues cuando ejecutes
assertEquals("Hello Mockito From Responsitory", helloService.get());

Realmente estás llamando al mock autoinyectado anteriormente.

TDD Controller segunda aproximación

En este ultimo caso tendríamos un aproximación más orientada a un test de integración. Ya que estaríamos haciendo una llamada que invocaría todas las capas, si nos quisiéramos quedar en la primera simplemente es hacer un mock del Service.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class MainControllerTest {
// bind the above RANDOM_PORT
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
public void getHello() throws Exception {
ResponseEntity<String> response = restTemplate.getForEntity(
new URL("http://localhost:" + port + "/hello").toString(), String.class);
assertEquals("Hello JUnit 5", response.getBody());
}
}

REPOSITORY

La anotación @Repository es la que marca que la class sea descubierta y registrada con el application context. La anotación es de propósito general y se puede aplicar sobre las clases DAO y los repositorios de estilo DDD.
¿Cómo lo vamos a Testear?
Haremos uso de la anotación @DataJpaTest que se centra solamente en los componentes JPA.

Normalmente haremos uso de una base de datos en memoria H2, para realizar dichos tests.

Esta parte profundizaremos en una segunda parte de este mismo manual.

Referencias

Este manual es una recopilación de otros, los cuales sin animo de plagio, están aquí recogidos:

Share This!



No hay comentarios:

Publicar un comentario

Nota: solo los miembros de este blog pueden publicar comentarios.