0. Punto de partida
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.
1. Configuración
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:
- @TestConfiguration: como hemos mencionado anteriormente sirve para que en los test se ejecute esta configuración
- Variable PORT: Establece el puerto en el que escuchará las peticiones MockServer, se verá en el siguiente apartado
- WebClient.Builder: Necesario para que levante un bean builder y podamos construir en test objetos WebClient
- WebClient webClient: Define la construcción de un WebClient concreto, deberemos tener tantos como WebClient tenga la configuración original.
A la hora de crear un WebClient, la clave está en la baseUrl tiene que ser siempre "http:://localhost:PUERTO, ya que haremos llamadas locales a MockServer en el puerto especificado. Si ponemos cualquier otra URL NO funcionará.
- Si tenemos varios clientes, deberán apuntar a la misma baseUrl, si hay conflicto de rutas, podemos ampliar baseUrl con algún sufijo para diferenciarlas, por ejemplo: http://localhost:XXX/webservice1, como no son llamadas reales la ruta base no es importante, salvo que siempre tiene que ser contra localhost.
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 MiTestConServiciosExternosTest {
...
...
@Autowired
private MiServivico servicio;
// private static MockServer mock;
private static MockWebServer mockServer;
@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 ) {
log.error("Error inicianzo mockserver",e)
}
}
@AfterAll
static void finalizaMockServer() {
if ( mockServer != null ) {
// mockServer.stop();
try {
mockServer.shutdown();
} catch ( IOException e ) {
log.error("Error deteniendo mockserver",e)
}
}
}
@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
- Anotación @Import( WebClientTestConfiguration.class ): Con esta anotación le decimos a nuestra clase de test que use la configuracíon indicada en WebClientTestConfiguration.class
- Creamos un método con @BeforeAll en el que se crea e inicializa MockWeb server en el mismo puerto que se especifica en nuestra configuración de WebClients
- mockServer.enqueue( ... ): Con esta línea encolamos (por orden) las diferentes respuestas que deben devolver todos los servicios externos a los que llamemos. Mockserver recibirá todas las peticiones Http que hagan los servicios y devolverá la respuesta que tenga en la cola.
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.