Muchas de nuestras aplicaciones hacen llamadas a otros servicios externos lo cual es un problema cuando queremos testear nuestra aplicación ya que o bien dichos servicios pueden no responder como se espera (debido a que son entornos de desarrollo) o bien no queremos realizar invocaciones que supongan cambios en las aplicaciones externas durante la fase de testing.
Para evitar esta problemática debemos mockear nuestras invocaciones a todos los servicios externos para que devuelvan una respuesta concreta. SpringBoot tenemos a nuestra disposición diferentes mecanismos de mocking siendo MockWebServer el recomendado por Spring para hacer esta tarea y es el que explicaremos en esta guía.
Por lo general nuestro proyecto tendrá la configuración de los diferentes WebClient que use
@Configuration
public class WebClientConfig {
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
@Bean
public WebClient webClient1(WebClient.Builder webClientBuilder) {
return webClientBuilder.baseUrl("http://aplicacionexterna1.um.es/").build();
}
@Bean
public WebClient webClient2(WebClient.Builder webClientBuilder) {
return webClientBuilder.baseUrl("http://aplicacionexterna2.um.es/").build();
}
} |
Ésta configuración establece los clientes a los servicios externos y cada servicio usará internamente el que necesite. Por tanto desde los test no tendremos acceso directo a modificar ninguna url base, por lo que nuestro objetivo será sustituir los WebClient utilizados por unos nuevos que no ataquen al servicio real y, en este punto, es donde se aplica el sistema de mocking basado en MockWebServer.
El primer paso para la configuración de nuestro MockWebServer es establecer las dependencias maven sólo para el scope de test. Si usamos el arquetipo fundewebjs-api-parent no hará falta establecer la versión de la librería a utilizar
<!-- Mock server --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>mockwebserver</artifactId> <scope>test</scope> </dependency> |
Una vez establecidas las dependencias deberé crearme una clase de configuración sólo para test que sobrescriba la configuración que tenga mi aplicación, para ello haremos uso de la anotacion @TestConfiguration creando una clase con dicha anotación, donde a su vez definiremos los nuevos WebClient de la siguiente manera:
@TestConfiguration
public class WebClientTestConfiguration {
public static final int PORT = 1080;
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
@Bean
public WebClient webClient( WebClient.Builder webClientBuilder ) {
return webClientBuilder.baseUrl( String.format( "http://localhost:%s", PORT ) ).defaultHeader( HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON ).
defaultHeader( HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON ).build();
}
}
|
En esta clase tenemos varios elementos a tener en cuenta:
Una vez tenemos configurada nuestra configuración de test, con los WebClient preparados podemos comenzar la implementación de nuestra clase de test. Un ejemplo de implementación sería el siguiente
@TestPropertySource( locations = "classpath:application-test.properties" )
@ContextConfiguration( classes = {MiServivico.class} )
@SpringBootTest
@Import( WebClientTestConfiguration.class )
@TestInstance( TestInstance.Lifecycle.PER_CLASS )
class ServicioGestorExpedientesTest {
@Autowired
private MiServivico servicio;
// private static MockServer mock;
private static MockWebServer mockServer;
// URLs
private static final String LOGIN = "/public/recurso";
@BeforeAll
public void iniciaMockServer() {
//Creamos una instancia de mockServer
mockServer = new MockWebServer();
try {
//Arrancamos mockServer en el puerto que hemos indicado en la configuración
mockServer.start( WebClientTestConfiguration.PORT );
} catch ( IOException e ) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@AfterAll
static void finalizaMockServer() {
if ( mockServer != null ) {
// mockServer.stop();
try {
mockServer.shutdown();
} catch ( IOException e ) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
@Test
void miTest() throws Exception {
String datosARecibir1 = "json1";
String datosARecibir2 = "json2";
//Encolamos las respuestas a recibir
mockServer.enqueue( new MockResponse().setBody( datosARecibir1 ).addHeader( "Content-Type", "application/json" ) );
mockServer.enqueue( new MockResponse().setBody( datosARecibir2 ).addHeader( "Content-Type", "application/json" ) );
String salida = servicio.metodoServicioConWebClient();
assertThat( salida ).isEqualTo( "Respuesta esperada1" );
salida = servicio.metodoServicioConWebClient();
assertThat( salida ).isEqualTo( "Respuesta esperada2" );
}
...
...
} |
Del código anterior los detalles importantes son
Con este código ya podremos emular llamadas a servicios externos y MockServer irá devolviendo las peticiones que tenga almacenadas. Hay que analizar cuidadosamente las respuestas que esperamos y el orden en el que deben llegar para que los test funcionen de manera correcta.