Guía para pruebas unitarias API REST Spring Boot

En este tutorial, veremos cómo realizar pruebas unitarias en las API REST de Spring Boot con ejemplos, utilizando JUnit, Mockito y MockMVC.

Introducción

Probar el sistema es una fase importante en un Ciclo de vida de desarrollo de software (SDLC). Las pruebas promueven la confiabilidad y la solidez del código y garantizan que el software de alta calidad se entregue a los clientes si se implementa correctamente.

A las pruebas se les ha dado más importancia desde que Test-Driven Development (TDD) se convirtió en un proceso destacado en el desarrollo de software. El desarrollo basado en pruebas implica convertir los requisitos en casos de prueba y utilizar estos casos de prueba para controlar la calidad del código. El código se considerará inaceptable si falla alguno de los casos de prueba declarados en un sistema, y ​​cuantos más casos de prueba cubran los requisitos del producto, mejor. El código base se alarga considerablemente pero refuerza el hecho de que el sistema cumple con los requisitos establecidos.

Las API REST generalmente se prueban rigurosamente durante las pruebas de integración. Sin embargo, un buen desarrollador debe probar los puntos finales REST incluso antes de la integración en sus Pruebas unitarias, ya que son una parte vital del código, ya que es el único punto de acceso de cada entidad que desea hacer uso de los servicios en el servidor. .

Esta guía demostrará cómo implementar pruebas unitarias para API REST en un entorno Spring Boot. Este artículo se centra en probar la capa empresarial que consta de las API, los puntos finales y los controladores dentro del código base.

Requisitos

Para este tutorial, necesitaría las siguientes especificaciones:

  • Arranque de primavera v2.0+ -JDK v1.8+
  • Unidad 5 - El marco de prueba más popular y ampliamente utilizado para Java.
  • Mockito - Marco de propósito general para servicios y objetos de simulación y stubbing.
  • MockMVC - Módulo de Spring para realizar * pruebas de integración durante las pruebas unitarias*.
  • Lombok - Biblioteca de conveniencia para reducir el código repetitivo.
  • Cualquier IDE que admita Java y Spring Boot (IntelliJ, VSC, NetBeans, etc.)
  • Cartero, curl o cualquier cliente HTTP

If you're still not quite comfortable building a REST API with Spring Boot - read our Guía para la creación de API REST de Spring Boot.

Usaremos Lombok como una biblioteca de conveniencia que genera automáticamente getters, setters y constructores, y es completamente opcional.

Configuración del proyecto

La forma más fácil de comenzar con un proyecto básico de Spring Boot es a través de Spring Initializr:

spring initializr

Aparte de estos, necesitaremos agregar un par de dependencias adicionales en el archivo pom.xml.

Adición de dependencias de pruebas unitarias

Avancemos y agreguemos las dependencias necesarias para las pruebas unitarias.

Para JUnit 5, la última versión, necesitaríamos excluir JUnit 4 de la dependencia spring-boot-starter-test porque agrega JUnit 4 de manera predeterminada. Para agregar JUnit 5 a su proyecto, agregue junit-jupiter-engine a sus dependencias en su archivo pom.xml principal después de excluir JUnit 4 de la dependencia springboot-starter-test.

MockMVC ya está incluido dentro de spring-boot-starter-test de forma predeterminada, por lo que un>spring-boot-starter-testless lo excluye y usa otra versión, entonces está listo para comenzar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!-- ...other dependencies -->
<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>

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-engine</artifactId>
  <scope>test</scope>
</dependency>

Además de JUnit 5, también necesitamos agregar dependencias para habilitar Mockito en su sistema. Para esto, simplemente agregue mockito-core a sus dependencias y coloque el valor test como el alcance de esta dependencia:

1
2
3
4
5
6
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <scope>test</scope>
</dependency>
<!-- ...other dependencies -->

Nota: Si no especifica la versión para sus dependencias, simplemente obtendrá la última versión estable disponible de esa dependencia del repositorio desde el que está descargando.

Con esto, ahora podemos proceder a codificar el dominio y las capas de persistencia.

Capas de dominio y persistencia {#capas de dominio y persistencia}

Capa de dominio: creación de un modelo de registro de paciente

La entidad de muestra que usaremos a lo largo del tutorial será de registros de pacientes que contengan algunos campos típicos para un registro de paciente.

No olvide anotar su clase de modelo con @Entity para especificar que la clase está asignada a una tabla en la base de datos. La anotación @Table también se puede especificar para asegurarse de que la clase apunte a la tabla correcta.

Aparte de estas dos anotaciones, incluya las anotaciones de la utilidad Lombok (@Data, @No/AllArgsConstructor, @Builder) para que no tenga que declarar sus getters, setters y constructores, ya que Lombok ya lo hace por usted. .

Los campos String y Integer se anotan con @NonNull para evitar que tengan un valor nulo o vacío para propósitos de validación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Entity
@Table(name = "patient_record")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PatientRecord {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long patientId;
    
    @NonNull
    private String name;
 
    @NonNull
    private Integer age;
    
    @NonNull 
    private String address;
}

Capa de persistencia: creación de un repositorio de registros de pacientes

El siguiente paso es crear un repositorio JPA para proporcionar métodos para recuperar y manipular fácilmente los registros de pacientes en la base de datos, sin la molestia de la implementación manual.

Anotemos una interfaz con @Repository y ampliemos JpaRepository para crear una interfaz de repositorio JPA que funcione correctamente. Para este tutorial, el repositorio JPA no tendrá ningún método personalizado, por lo que el cuerpo debe estar vacío:

1
2
@Repository
public interface PatientRecordRepository extends JpaRepository<PatientRecord, Long> {}

Ahora que hemos construido nuestro dominio simple y la capa de persistencia, pasemos a codificar los componentes para nuestra capa empresarial.

Capa empresarial {#capa empresarial}

La capa empresarial está compuesta por controladores que permiten la comunicación con el servidor y brinda acceso a los servicios que brinda.

Para este tutorial, hagamos un controlador que exponga 4 puntos finales REST simples, uno para cada operación CRUD: Crear, Leer, Actualizar y Eliminar.

Creación de instancias de una clase de controlador - PatientRecordController

En primer lugar, anote su clase de controlador con la anotación @RestController para informar al DispatcherServlet que esta clase contiene métodos de asignación de solicitudes.

Si no has trabajado con Rest Controllers antes, lee nuestra guía sobre Las anotaciones @Controller y @RestController.

Para proporcionar servicios CRUD para los métodos, declare la interfaz PatientRecordRepository dentro de la clase del controlador y anótelo con @Autowired para inyectar implícitamente el objeto para que no tenga que crear una instancia manualmente.

También puede anotar la clase con @RequestMapping con una propiedad value para inicializar una ruta base para todos los métodos de asignación de solicitudes dentro de la clase. Establezcamos la propiedad value en /patientRecord para que la ruta base sea intuitiva:

1
2
3
4
5
6
@RestController
@RequestMapping(value = "/patient")
public class PatientRecordController {
    @Autowired PatientRecordRepository patientRecordRepository;
    // CRUD methods to be added
}

Ahora, creemos varios métodos que constituyan la funcionalidad CRUD que probaremos unitariamente.

Recuperación de pacientes: controlador de solicitudes GET

Vamos a crear dos métodos GET diferentes: uno para obtener todos los registros de pacientes dentro de la base de datos y otro para obtener un solo registro con una ID de paciente.

Para especificar que un método está mapeado por GET, anótelo con la anotación @GetMapping:

1
2
3
4
5
6
7
8
9
@GetMapping
public List<PatientRecord> getAllRecords() {
    return patientRecordRepository.findAll();
}

@GetMapping(value = "{patientId}")
public PatientRecord getPatientById(@PathVariable(value="patientId") Long patientId) {
    return patientRecordRepository.findById(patientId).get();
}

If you're unfamiliar with the derived variants of @RequestMapping - you can read our guide on Anotaciones de primavera: @RequestMapping y sus variantes.

Dado que el método getPatientById() necesita un parámetro (patientId), lo proporcionaremos a través de la ruta, anotándolo con @PathVariable y proporcionando la propiedad value de la variable. Además, establezca la propiedad value de la anotación @GetMapping para asignar la variable de ruta a su lugar real en la ruta base.

Creación de pacientes: controlador de solicitudes POST

Agregar nuevos registros de pacientes necesitará un método de mapeo ‘POST’. El método aceptará un parámetro PatientRecord anotado por @RequestBody y @Valid. La anotación @Valid asegura que todas las restricciones dentro de la base de datos y en la clase de entidad se cotejen antes de manipular los datos.

If you're unfamiliar with the process of deserializing HTTP requests to Java objects - read our guide on Cómo obtener el cuerpo de la publicación HTTP en Spring Boot con @RequestBody:

1
2
3
4
@PostMapping
public PatientRecord createRecord(@RequestBody @Valid PatientRecord patientRecord) {
    return patientRecordRepository.save(patientRecord);
}

Antes de continuar con los otros métodos de solicitud, creemos una sola excepción general para todas las excepciones encontradas en el código base y llamémosla InvalidRequestException. Para el código de estado, usemos el código de estado BAD_REQUEST 400.

Para manejar las excepciones y convertirlas en un código de estado para volver a la persona que llama, declaremos una clase de excepción simple que extienda la clase RuntimeException:

1
2
3
4
5
6
@ResponseStatus(HttpStatus.BAD_REQUEST)
class InvalidRequestException extends RuntimeException {
    public InvalidRequestException(String s) {
        super(s);
    }
}

Actualización de pacientes: controlador de solicitudes PUT

Para manejar las actualizaciones, para el método PUT, anotemos con un @PutMapping y solicitemos un parámetro anotado por @RequestBody que contenga el PatientRecord actualizado, similar al mapeo POST.

Querremos asegurarnos de que el registro existe para fines de validación mediante el patientId. Dado que se trata de una solicitud PUT, el registro que se actualizará debe existir dentro de la base de datos, de lo contrario, esta es una solicitud no válida. Además, lanza una InvalidRequestException si el cuerpo de la solicitud o el campo patientId es nulo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@PutMapping
public PatientRecord updatePatientRecord(@RequestBody PatientRecord patientRecord) throws NotFoundException {
    if (patientRecord == null || patientRecord.getPatientId() == null) {
        throw new InvalidRequestException("PatientRecord or ID must not be null!");
    }
    Optional<PatientRecord> optionalRecord = patientRecordRepository.findById(patientRecord.getPatientId());
    if (optionalRecord.isEmpty()) {
        throw new NotFoundException("Patient with ID " + patientRecord.getPatientId() + " does not exist.");
    }
    PatientRecord existingPatientRecord = optionalRecord.get();

    existingPatientRecord.setName(patientRecord.getName());
    existingPatientRecord.setAge(patientRecord.getAge());
    existingPatientRecord.setAddress(patientRecord.getAddress());
    
    return patientRecordRepository.save(existingPatientRecord);
}

Eliminación de pacientes: DELETE Request Handler

Ahora, también querremos poder eliminar pacientes. Este método será anotado por @DeleteMapping y aceptará un parámetro patientId y eliminará al paciente con esa ID si existe. El método devolverá una excepción y un código de estado 400 si el paciente no existe. Al igual que el método GET que recupera a un paciente por ID, agregue una propiedad value a la anotación @DeleteMapping, así como @PathVariable:

1
2
3
4
5
6
7
@DeleteMapping(value = "{patientId}")
public void deletePatientById(@PathVariable(value = "patientId") Long patientId) throws NotFoundException {
    if (patientRecordRepository.findById(patientId).isEmpty()) {
        throw new NotFoundException("Patient with ID " + patientId + " does not exist.");
    }
    patientRecordRepository.deleteById(patientId);
}

¡Ahora, nuestra capa empresarial está preparada y lista! Podemos seguir adelante y escribir pruebas unitarias para ello.

Si desea leer una guía más detallada para crear API REST en Spring Boot, lea nuestra [Guía para la creación de API REST de Spring Boot](/construir-una-api-spring-boot-rest-con -la-guía-completa-de-java/).

Pasemos a crear pruebas unitarias para las API REST en nuestra clase de controlador usando JUnit, Mockito y MockMVC.

Pruebas unitarias API REST Spring Boot

MockMVC es una solución para permitir pruebas unitarias de capa web. Por lo general, la prueba de las API REST se realiza durante la prueba de integración, lo que significa que la aplicación debe ejecutarse en un contenedor para probar si los puntos finales funcionan o no. MockMVC permite probar la capa web (también conocida como capa empresarial o capa de controlador) durante las pruebas unitarias con las configuraciones adecuadas, pero sin la sobrecarga de tener que implementar la aplicación.

Tener pruebas unitarias para la capa web también aumentará significativamente la cobertura del código de prueba para su aplicación y se reflejará en herramientas como Sonar y JaCoCo.

El directorio de prueba unitaria generalmente se encuentra en el mismo directorio de origen bajo un directorio prueba/java/paquete. De forma predeterminada, la estructura del archivo de prueba de unidad se vería así:

1
2
3
4
5
6
7
Project:
├─src
  ├───main
     ├───java
     └───resources
  └───test
      └───java

También es una buena práctica y una convención estándar nombrar sus clases de prueba de la misma manera que los controladores que está probando, con un sufijo -Prueba. Por ejemplo, si queremos probar PatientRecordController, crearemos una clase PatientRecordControllerTest en el paquete apropiado en src/test/java.

En lugar de anotar su clase de prueba con @SpringBootTest, usaremos la anotación @WebMvcTest para que las dependencias que se cargarán cuando ejecute la clase de prueba sean las que afecten directamente a la clase del controlador. Los servicios, repositorios y conexiones de base de datos no se configurarán ni cargarán una vez que se ejecute la prueba, por lo que tendrá que simular todos estos componentes con la ayuda de Mockito.

En este caso, solo necesitamos especificar un solo controlador: PatientRecordController.class, para la anotación @WebMvcTest. Si hay varios controladores inyectados en una sola clase de prueba, sepárelos con una coma , y envuélvalos con un par de llaves {}:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@WebMvcTest(PatientRecordController.class)
public class PatientRecordControllerTest {
    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper mapper;
    
    @MockBean
    PatientRecordRepository patientRecordRepository;
    
    PatientRecord RECORD_1 = new PatientRecord(1l, "Rayven Yor", 23, "Cebu Philippines");
    PatientRecord RECORD_2 = new PatientRecord(2l, "David Landup", 27, "New York USA");
    PatientRecord RECORD_3 = new PatientRecord(3l, "Jane Doe", 31, "New York USA");
    
    // ... Test methods TBA
}

Aquí, declaramos un objeto MockMvc y lo anotamos con @Autowired, que está permitido en este contexto porque MockMvc se configura automáticamente y forma parte de las dependencias que se cargan para esta clase de prueba. También hemos conectado automáticamente el objeto ObjectMapper; esto se utilizará más adelante.

La interfaz PatientRecordRepository se usa en todos los puntos finales de la API, por lo que la hemos burlado con @MockBean. Finalmente, hemos creado algunas instancias de PatientRecord con fines de prueba.

Prueba unitaria de los controladores de solicitudes GET

Ahora, podemos seguir adelante y hacer nuestro primer caso de prueba, también conocido como prueba unitaria. Probaremos el método getAllRecords(), nuestro controlador de solicitudes GET. Para cada prueba unitaria, crearemos un solo método que pruebe otro. Cada prueba unitaria se anota con @Test para que JUnit pueda recogerlas y ponerlas en una lista de todas las pruebas que deben ejecutarse:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Test
public void getAllRecords_success() throws Exception {
    List<PatientRecord> records = new ArrayList<>(Arrays.asList(RECORD_1, RECORD_2, RECORD_3));
    
    Mockito.when(patientRecordRepository.findAll()).thenReturn(records);
    
    mockMvc.perform(MockMvcRequestBuilders
            .get("/patient")
            .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", hasSize(3)))
            .andExpect(jsonPath("$[2].name", is("Jane Doe")));
}

El método de cadena Mockito when().thenReturn() se burla de la llamada al método getAllRecords() en el repositorio JPA, por lo que cada vez que se llama al método dentro del controlador, devolverá el valor especificado en el parámetro de el método thenReturn(). En este caso, devuelve una lista de tres registros de pacientes preestablecidos, en lugar de realizar una llamada a la base de datos.

MockMvc.perform() acepta una MockMvcRequest y se burla de la llamada API dados los campos del objeto. Aquí, construimos una solicitud a través de MockMvcRequestBuilders y solo especificamos la ruta GET y la propiedad contentType ya que el extremo de la API no acepta ningún parámetro.

Después de ejecutar perform(), los métodos andExpect() se encadenan posteriormente y se comparan con los resultados devueltos por el método. Para esta llamada, hemos establecido 3 aserciones dentro de los métodos andExpect(): que la respuesta devuelve un código de estado 200 o OK, la respuesta devuelve una lista de tamaño 3 y la tercera El objeto PatientRecord de la lista tiene una propiedad name de Jane Doe.

Los métodos referenciados estáticamente aquí - jsonPath(), hasSize() y is() pertenecen a las clases MockMvcResultMatchers y Matchers respectivamente:

1
2
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.*;

Por supuesto, puede hacer referencia estática a ellos:

1
2
.andExpect(MockMvcResultMatchers.jsonPath("$", Matchers.hasSize(3)))
.andExpect(MockMvcResultMatchers.jsonPath("$[2].name", Matchers.is("Jane Doe")));

Sin embargo, si tiene muchas declaraciones andExpect() encadenadas, esto se volverá repetitivo y molesto con bastante rapidez.

Nota: Todas estas afirmaciones no deben fallar para que se apruebe la prueba unitaria. Ejecutar este código da como resultado:

Ahora, agreguemos otro caso de prueba para el método getPatientById(). Justo debajo de la prueba unitaria anterior, podemos escribir una nueva:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Test
public void getPatientById_success() throws Exception {
    Mockito.when(patientRecordRepository.findById(RECORD_1.getPatientId())).thenReturn(java.util.Optional.of(RECORD_1));

    mockMvc.perform(MockMvcRequestBuilders
            .get("/patient/1")
            .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", notNullValue()))
            .andExpect(jsonPath("$.name", is("Rayven Yor")));
}

Aquí, estamos comprobando si el resultado es nulo, afirmando que no lo es y comprobando si el campo nombre del objeto devuelto es igual a "Rayven Yor". Si ejecutamos toda la clase PatientRecordControllerTest ahora, nos recibiría con:

Prueba unitaria de los controladores de solicitudes POST

Ahora que hemos probado la capacidad de las API para recuperar registros individuales e identificables, así como una lista de todos los registros, probemos su capacidad para mantener registros. El controlador de solicitudes POST acepta una solicitud POST y asigna los valores proporcionados a un POJO PatientRecord a través de la anotación @RequestBody. Nuestra unidad de prueba también aceptará JSON y mapeará los valores en un POJO PatientRecord a través del ObjectMapper que hemos autoconectado antes. También guardaremos una referencia al MockHttpServletRequestBuilder devuelto después de que MockMvcRequestBuilders lo haya generado para que podamos probar los valores devueltos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void createRecord_success() throws Exception {
    PatientRecord record = PatientRecord.builder()
            .name("John Doe")
            .age(47)
            .address("New York USA")
            .build();

    Mockito.when(patientRecordRepository.save(record)).thenReturn(record);

    MockHttpServletRequestBuilder mockRequest = MockMvcRequestBuilders.post("/patient")
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaType.APPLICATION_JSON)
            .content(this.mapper.writeValueAsString(record));

    mockMvc.perform(mockRequest)
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", notNullValue()))
            .andExpect(jsonPath("$.name", is("John Doe")));
    }

Ejecutar la clase una vez más da como resultado:

Prueba unitaria de los controladores de solicitudes PUT

El controlador de solicitudes PUT tiene un poco más de lógica que los dos anteriores. Comprueba si hemos proporcionado una identificación, lo que genera una excepción si falta. Luego, verifica si la ID realmente pertenece a un registro en la base de datos, lanzando una excepción si no es así. Solo entonces actualiza un registro en la base de datos, si la ID no es “nula” y pertenece a un registro.

Crearemos tres métodos de prueba para verificar si las tres facetas de este método funcionan: una para el éxito y otra para cada uno de los estados erróneos que pueden ocurrir:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void updatePatientRecord_success() throws Exception {
    PatientRecord updatedRecord = PatientRecord.builder()
            .patientId(1l)
            .name("Rayven Zambo")
            .age(23)
            .address("Cebu Philippines")
            .build();

    Mockito.when(patientRecordRepository.findById(RECORD_1.getPatientId())).thenReturn(Optional.of(RECORD_1));
    Mockito.when(patientRecordRepository.save(updatedRecord)).thenReturn(updatedRecord);

    MockHttpServletRequestBuilder mockRequest = MockMvcRequestBuilders.post("/patient")
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaType.APPLICATION_JSON)
            .content(this.mapper.writeValueAsString(updatedRecord));

    mockMvc.perform(mockRequest)
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", notNullValue()))
            .andExpect(jsonPath("$.name", is("Rayven Zambo")));
}

Sin embargo, en los casos en que los datos de entrada no sean correctos o la base de datos simplemente no contenga la entidad que estamos tratando de actualizar, la aplicación debería responder con una excepción. Probemos que:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Test
public void updatePatientRecord_nullId() throws Exception {
    PatientRecord updatedRecord = PatientRecord.builder()
            .name("Sherlock Holmes")
            .age(40)
            .address("221B Baker Street")
            .build();

    MockHttpServletRequestBuilder mockRequest = MockMvcRequestBuilders.post("/patient")
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaType.APPLICATION_JSON)
            .content(this.mapper.writeValueAsString(updatedRecord));

    mockMvc.perform(mockRequest)
            .andExpect(status().isBadRequest())
            .andExpect(result ->
                assertTrue(result.getResolvedException() instanceof PatientRecordController.InvalidRequestException))
    .andExpect(result ->
        assertEquals("PatientRecord or ID must not be null!", result.getResolvedException().getMessage()));
    }

@Test
public void updatePatientRecord_recordNotFound() throws Exception {
    PatientRecord updatedRecord = PatientRecord.builder()
            .patientId(5l)
            .name("Sherlock Holmes")
            .age(40)
            .address("221B Baker Street")
            .build();

    Mockito.when(patientRecordRepository.findById(updatedRecord.getPatientId())).thenReturn(null);

    MockHttpServletRequestBuilder mockRequest = MockMvcRequestBuilders.post("/patient")
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaType.APPLICATION_JSON)
            .content(this.mapper.writeValueAsString(updatedRecord));

    mockMvc.perform(mockRequest)
            .andExpect(status().isBadRequest())
            .andExpect(result ->
                assertTrue(result.getResolvedException() instanceof NotFoundException))
    .andExpect(result ->
        assertEquals("Patient with ID 5 does not exist.", result.getResolvedException().getMessage()));
}

Dado que hemos mapeado InvalidRequestException con un @ResponseStatus(HttpStatus.BAD_REQUEST), lanzar la excepción hará que el método devuelva un HttpStatus.BAD_REQUEST. Aquí, hemos probado la capacidad de nuestra API REST para devolver códigos de estado apropiados cuando se enfrentan a datos defectuosos o cuando alguien intenta actualizar una entidad inexistente.

Prueba unitaria de los controladores de solicitudes DELETE

Finalmente, probemos la funcionalidad de nuestro controlador de solicitudes DELETE - creando una prueba para el resultado exitoso y una prueba para el resultado fallido:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void deletePatientById_success() throws Exception {
    Mockito.when(patientRecordRepository.findById(RECORD_2.getPatientId())).thenReturn(Optional.of(RECORD_2));

    mockMvc.perform(MockMvcRequestBuilders
            .delete("/patient/2")
            .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk());
}

@Test
public void deletePatientById_notFound() throws Exception {
    Mockito.when(patientRecordRepository.findById(5l)).thenReturn(null);

    mockMvc.perform(MockMvcRequestBuilders
            .delete("/patient/2")
            .contentType(MediaType.APPLICATION_JSON))
    .andExpect(status().isBadRequest())
            .andExpect(result ->
                    assertTrue(result.getResolvedException() instanceof NotFoundException))
    .andExpect(result ->
            assertEquals("Patient with ID 5 does not exist.", result.getResolvedException().getMessage()));
}

Ahora, usemos Maven para limpiar el proyecto, compilarlo y ejecutar las pruebas.

Ejecutar el programa con pruebas unitarias

En primer lugar, debemos agregar el complemento Maven Surefire en el archivo pom.xml para que podamos ejecutar el comando mvn clean test. También agregaremos una etiqueta de configuración adicional para incluir la clase de prueba PatientRecordControllerTest.java para incluirla en las pruebas de Maven:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<plugins>
    <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.21.0</version>
        <configuration>
            <includes>
                <include>PatientRecordControllerTest.java</include>
            </includes>
        </configuration>
    </plugin>
    
    <!-- Other plugins -->
</plugins>

Luego, en el directorio de nuestro proyecto, usando una terminal, ejecutemos:

1
$ mvn clean test

Lo que resulta en:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.example.demo.PatientRecordControllerTest
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.504 s - in com.example.demo.PatientRecordControllerTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  5.633 s
[INFO] Finished at: 2021-05-25T19:51:24+02:00
[INFO] ------------------------------------------------------------------------

Conclusión

En esta guía, hemos analizado cómo crear y probar una API REST Spring Boot con funcionalidad CRUD usando JUnit, Mockito y MockMvc.