Ejemplos de Testing en Spring Boot

Testing-Spring-Boot

Testing-Spring-Boot


Spring Boot nos proporciona diferentes funcionalidades y facilidades para poder realizar tanto test unitarios como integrados en nuestras aplicaciones o servicios. En esta entrada sobre ejemplos de testing en Spring Boot vamos a ver como podemos realizar testing haciendo uso de las anotaciones y funcionalidades que nos proporciona Spring Boot.

¿Qué son los test de integración?

Podríamos definir los test de integración o Integration Test, como aquella parte del testing de nuestras aplicaciones en donde los módulos son integrados lógicamente y testeados como un grupo. Por lo general en nuestras aplicaciones o servicios vamos a tener diferentes módulos o capas. El objetivo de este tipo de test es ver y analizar los posibles defectos y errores en la interacción entre las diferentes capas y ver como se integran entre ellas.

¿Qué son los test unitarios?

Podríamos definir un test unitario como la prueba que nos va a permitir verificar una unidad o parte de nuestro código. Es decir, probar y verificar partes aisladas de nuestro código para asegurarnos que la funcionalidad que hemos implementado funciona correctamente.

Test de Integración vs Test Unitarios

Un test unitario va a analizar o verificar una funcionalidad aislada, sin interacción con otros módulos o con otras integraciones.

La diferencia principal con un Test Integrado es básicamente, que con este último, vamos a realizar una prueba y a verificar el sistema en su conjunto, para detectar esos posibles errores en las integraciones.

Podríamos, además, decir que con un test integrado también podemos analizar diferentes funcionalidades, y al llegar y abarcar diferentes módulos, se podrían realizar pruebas sobre funcionalidades de test unitarios.

Pirámide de Testing | Ejemplos de Testing en Spring Boot
Pirámide de Testing

Dependencias Maven para los Test Integrados en Spring Boot

La librería imprescindible que nuestras aplicaciones Spring Boot van a necesitar para poder realizar testing es spring-boot-starter-test.

Esta librería nos va añadir las funcionalidades que necesitamos para realizar tareas de testing en nuestra aplicación.

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

Test de Integración en Spring Boot con @SpringBootTest

Cuando creemos nuestros test de integración, la mejor aproximación será separarlos de los test unitarios, y en clases aparte.

Spring Boot nos proporciona la anotación @SpringBootTest para poder inicializar nuestra aplicación en función de unas propiedades que le proporcionemos.

Esta anotación nos permitirá crear un ApplicationContext  de nuestra aplicación. Vamos a ver un ejemplo:

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
public class BookControllerIT {

  @Autowired
  private MockMvc mockMvc;

En la creación de la clase anterior hemos añadido diversas anotaciones que nos facilitará la creación de nuestro test de integración:

  • SpringBootTest: Nos va a crear un contenedor de nuestra aplicación creando un ApplicationContext.
  • ExtendWith para Junit5: Para habilitar el soporte para poder hacer uso de JUnit 5.
  • AutoconfigureMockMvc: Nos permitirá hacer uso de MockMvc para nuestros test integrados, de manera que podamos realizar peticiones HTTP.

Hasta la versión 2.1 de Spring Boot se añadía @ExtendWith para decirle a JUnit 5 que habilite el soporte de Spring. Desde Spring Boot 2.1, ya no se necesita añadir esta anotación porque está incluido como una meta anotación en las anotaciones de prueba de Spring Boot como @DataJpaTest, @WebMvcTest y @SpringBootTest.

Como podemos ver la anotación @SpringBootTest, nos arranca nuestra aplicación con un ApplicationContext para test. Esta anotación podrá ir sola o con otros parámetros, por ejemplo:

  • webEnvironment = WebEnvironment.RANDOM_PORT: Con esta opción nuestros test arrancaran en un puerto aleatorio que además no se encuentre ya en uso.
  • webEnvironment = WebEnvironment.DEFINED_PORT: Al incluir esta opción nuestros test arrancaran en un puerto predefinido.

Si quieres saber en que puerto te arranca la aplicación puedes hacer uso de @LocalServerPort 

Modificaciones y AutoConfiguraciones para nuestro ApplicationContext

El uso de @SpringBootTest, nos va a permitir incluir diferentes autoconfiguraciones para nuestros test (en el anterior punto ya hemos visto alguna), vamos a ver qué cosas podemos hacer en nuestros test:

Habilitar autoconfiguraciones

El uso de SpringBootTest para nuestros test, nos va a permitir crear diferentes autoconfiguraciones, vamos a mostrar algunas de las que podemos añadir:

  • @AutoconfigureMockMvc: Nos va a permitir añadir un MockMvc  a nuestro ApplicationContext, de esta manera podremos realizar peticiones HTTP contra nuestro controlador.
  • @AutoConfigureTestDatabase: Por lo general tendremos una Base de Datos embebida o en memoria, esta anotación, sin embargo, nos permitira realizar nuestros test contra una base de datos real.
  • @JsonTest: Haciendo uso de esta anotación, podremos verificar si nuestros serializadores y deserializadores de JSON funcionan de forma correcta.
  • @RestClientTest: Nos va a permitir verificar nuestros resttemplate, y junto con MockRestServiceServer , nos permitirá mockear las respuestas que llegan del restemplate.
  • @AutoConfigureWebTestClient: Nos permite verificar los endpoint del servidor, agrega WebTestClient al contexto.

Estas son algunas de las más comunes que se suelen aplicar en test de integración con Spring Boot, pero hay muchas más, si quieres puedes echar un vistazo aquí.

Configuraciones en test a través de Ficheros de Propiedades con ActiveProfiles

Al igual que hacemos en nuestra aplicación Spring Boot haciendo uso de diferentes perfiles a través de nuestras propiedades, por ejemploapplication-dev.ym, en nuestros test integrados podemos también cargar propiedades específicas. Para poder hacer uso de esta funcionalidad podemos añadir la anotación @ActiveProfiles.

Esta anotación debe llevar un fichero de propiedades asociado por ejemplo, application-test.yml; y en nuestro @ActiveProfiles tendrá asociado test.

# application-test.yml
hi: hi
@SpringBootTest
@ActiveProfiles("test")
class HiControllerIntegrationTest{

  @Value("${hi}")
  String sayHi;

  @Test
  void test(){
    assertThat(sayHi).isEqualTo("hi");
  }
}

Sobreescribiendo propiedades de configuración

El uso de ActiveProfiles nos va a ayudar para cargar un fichero de propiedades entero, pero en muchas ocasiones, vamos a necesitar únicamente sobreescribir una propiedad de nuestro fichero. Para esos casos podemos añadir «properties» y el campo a sobreescribir junto con nuestra anotación @SpringBootTest:

# application.yml
hi: hi
@SpringBootTest(properties = "hi=bye")
class HiControllerIntegrationTest{

  @Value("${hi}")
  String sayHi;

  @Test
  void test(){
    assertThat(hi).isEqualTo("bye");
  }
}

Como nuestro application.yml tiene esa propiedad, la sobreescribiremos haciendo uso de «Properties».

Modificando propiedades de Configuración con TestPropertySource

Otra manera de poder cargar propiedades en nuestro ApplicationContext, es haciendo uso de @TestPropertySource, con lo que podemos cargar un fichero customizado a nuestros test. Veamos un ejemplo:

# src/test/resources/hi.properties
hi=hi
bye=bye
@SpringBootTest
@TestPropertySource(locations = "/hi.properties")
class HiControllerIntegrationTest {

  @Value("${hi}")
  String hi;

  @Test
  void test(){
    assertThat(hi).isEqualTo("hi");
  }
}

Test de Integración con @DataJpaTest

La anotación @DataJpaTest nos va a permitir realizar test de integración para nuestras aplicaciones JPA. Esta anotación escanea por defecto aquellas clases anotadas con @Entity y los repositorio JPA y además configura la base de datos embebida de nuestros test.

Por defecto @DataJapTest nos hace nuestros test transaccionales y hará un rollback al finalizar el test.

@Entity
@Getter
@Setter
public class Book {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String title;

  private int price;

  private String isbn;
}
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {

}
@DataJpaTest
public class BookControllerDataJpaTestIT  {

  @Autowired
  private TestEntityManager entityManager;

  @Autowired
  private BookRepository bookRepository;

  @Test
  public void whenFindByName_thenReturnBook() {
    // given
    Book book = createBook();

    entityManager.persist(book);
    entityManager.flush();

    // when
    Book b = bookRepository.findByTitle("The Count of Monte Cristo").orElseThrow();

    // then
    assertThat(b.getTitle())
        .isEqualTo(book.getTitle());
  }

  private Book createBook() {

    Book book = new Book();
    book.setIsbn("1A2s-3f");
    book.setTitle("The Count of Monte Cristo");
    book.setPrice(34);

    return book;

  }

}

El uso de TestEntityManager es una alternativa al uso de JPA EntityManager para hacer uso de métodos de JPA en nuestros tests.

El ejemplo anterior hacemos un guardado en Base de Datos y a continuación lo recuperamos para verificar que el resultado es el correcto.

Test de integración con WebTestClient

Una de las funcionalidades que Spring 5 trajo consigo, fue la incorporación de WebFlux para proporcionarnos soporte para programación reactiva. Para aquellos escenarios en los que en nuestra aplicación hacemos uso de WebFlux, va a ser necesario utilizar WebTestClient para poder probar nuestra API.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class WebTestClientExampleTests {

    @Autowired
    private WebTestClient webClient;

    @Test
    public void exampleTest() {
      this.webClient.get().uri("/books/1").exchange().expectStatus().isOk()
      .expectBody(String.class).isEqualTo("Harry Potter");
    }

}

Testing en Spring Boot con MockMvc

Como hemos visto en algún ejemplo anterior, el uso de MockMvc nos va a permitir realizar peticiones HTTP a nuestra API o capa controlador.

MockMvc nos permite realizar peticiones HTTP con los diferentes verbos y añadir tanto las cabeceras como los parámetros que necesitemos. Para poder hacer uso de MockMvc lo que hay que tener en cuenta es añadir su autoconfiguraicón a través de la anotación @AutoConfigureMockMvc.

@AutoConfigureMockMvc
@SpringBootTest
public class BookControllerIT {

  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private ObjectMapper objectMapper;

  @Test
  void findById() throws Exception {
    var book = createBook();

    mockMvc.perform(
            MockMvcRequestBuilders.post("/api/books")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(book)))
        .andExpect(status().isCreated());

    var findById = mockMvc.perform(
            get("/api/books/1").accept(MimeTypeUtils.APPLICATION_JSON_VALUE))
        .andExpect(status().isOk())
        .andReturn();

    var b = objectMapper.readValue(findById.getResponse().getContentAsString(), Book.class);

    assert b.getIsbn().equalsIgnoreCase("1A2s-3f");

  }

  private Book createBook() {

    Book book = new Book();
    book.setIsbn("1A2s-3f");
    book.setTitle("The Count of Monte Cristo");
    book.setPrice(34);

    return book;

  }
}

Test de integración con TestRestTemplate

Cuando queremos realizar test integrados de nuestra aplicación, por lo general para realizar test integrados invocando a nuestra API por peticiones HTTP, es muy normal hacer uso de MockMvc. Aunque Spring nos proporciona un cliente HTTP para poder realizar estas peticiones, al igual que usamos RestTemplate, podemos hacer uso TestRestTemplate para nuestra parte de testing.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class StudentControllerTests {

    @LocalServerPort
    private int port;

    TestRestTemplate restTemplate = new TestRestTemplate();

    HttpHeaders headers = new HttpHeaders();

    @Test
    public void testCreateBook() throws Exception {
        HttpEntity<String> entity = new HttpEntity<String>(null, headers);

        ResponseEntity<String> response = restTemplate.exchange(
          createURLWithPort("/books"), HttpMethod.POST, entity, String.class);

        String actual = response.getHeaders().get(HttpHeaders.LOCATION).get(0);

        assertTrue(actual.contains("/books"));
    }    
}

Como podemos ver en el ejemplo anterior hemos hecho uso de TestRestTemplate para hacer una petición POST.

Test unitarios de nuestro controlador con @WebMvcTest

Vamos a hacer uso de @WebMvcTest para conseguir realizar un test unitario de nuestra capa Controlador. Esta capa es el punto de entrada a nuestra aplicación y el encargado de recibir las peticiones HTTP.

Spring nos brinda la anotación @WebMvcTest para facilitarnos los test unitarios en de nuestros controladores.

@RequestMapping("/api")
@RequiredArgsConstructor
@RestController
public class BookController {

  private final BookService bookService;

  @GetMapping("/books/{id}")
  public ResponseEntity<Book> getBookById(@PathVariable Long id) {

    return ResponseEntity.ok(bookService.findById(id));
  }

  @PostMapping("/books")
  public ResponseEntity<Book> saveBook(@RequestBody Book book) {

    var save = bookService.saveBook(book);

    return new ResponseEntity<>(HttpStatus.CREATED);

  }
}

Nuestro contralador llamará a la capa service en donde reside la lógica, por lo que vamos a hacer un Mock de esa capa:

@WebMvcTest(BookController.class)
public class BookControlerTest {

  @Autowired
  private MockMvc mockMvc;

  @MockBean
  private BookService service;

  @Autowired
  private ObjectMapper objectMapper;

  @Test
  public void givenBooks_whenGetBookById_thenReturnBook()
      throws Exception {

    Book book = createBook();

    given(service.findById(1L)).willReturn(book);

    var findById = mockMvc.perform(
            get("/api/books/1")
                .accept(MimeTypeUtils.APPLICATION_JSON_VALUE))
        .andExpect(status().isOk())
        .andReturn();

    var b = objectMapper.readValue(findById.getResponse().getContentAsString(), Book.class);

    assert b.getIsbn().equalsIgnoreCase("1A2s-3f");
  }

  private Book createBook() {

    Book book = new Book();
    book.setIsbn("1A2s-3f");
    book.setTitle("Numancia");
    book.setPrice(34);

    return book;

  }

}

Al hacer uso de la anotación @WebMvcTest nos va a autoconfigurar MockMvc para que podamos realizar peticiones HTTP a nuestro controlador.

Testing en Spring Boot con @MockBean

Por lo general nuestras aplicaciones o microservicios estarán compuestos de varias capas, por ejemplo, nuestra capa Controlador del ejemplo anterior depende de la capa Service. O nuestra capa Service depende de nuestro Repository. Por lo que para esos casos en los que necesitamos realizar pruebas unitarias de las diferentes capas de nuestra aplicación vamos a hacer uso de @MockBean.

@MockBean nos va a permitir hacer un mock de capas enteras de nuestra aplicación, para nuestro siguiente ejemplo vamos a hacer un mock para la capa repository de nuestra aplicación, ya que lo que nos interesa es únicamente la capa Service:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BookServiceTest {

  @Autowired
  private BookService bookService;

  @MockBean
  private BookRepository bookRepository;

  @Test
  public void testRetrieveBookWithMockRepository() throws Exception {

    Optional<Book> optStudent = Optional.of(createBook());

    when(bookRepository.findById(1L)).thenReturn(optStudent);

    assert bookService.findById(1L).getTitle().contains("Numancia");

  }

  private Book createBook() {

    Book book = new Book();
    book.setIsbn("1A2s-3f");
    book.setTitle("Numancia");
    book.setPrice(34);

    return book;

  }
}

Conclusión

En esta entrada sobre Testing en Spring Boot hemos visto las diferentes maneras que nos ofrece Spring para realizar labores de testing, tanto en test unitarios como en TestIntegrados.

@SpringBootTest, nos va a facilitar casi todas nuestras labores de testing arrancando un Application Context, que será bastante parecido al que tendremos con nuestra aplicación arrancada. Por otro lado, sino necesitamos arrancar un contexto de aplicación podemos evitar el uso de @SpringBootTest y hacer uso únicamente de @MockBean, o incluso si solo queremos hacer pruebas contra nuestra capa repository podemos usar @DataJpaTest.

Con todas las posibilidades de parametrización que nos ofrece Spring Boot no hay excusa para no hacer labores de testing.

Si quieres ver ver diferentes ejemplos puedes echar un vistazo a nuestro github de refactorizando aquí.

Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com o también nos puedes contactar por nuestras redes sociales Facebook o twitter y te ayudaremos encantados!


Deja una respuesta

Tu dirección de correo electrónico no será publicada.