Mostrando entradas con la etiqueta JUnit5. Mostrar todas las entradas
Mostrando entradas con la etiqueta JUnit5. Mostrar todas las entradas

jueves, 9 de abril de 2026

MongoDB & Testcontainers

Ya hemos probado otras veces el Testcontainers, aquí. Hoy veremos un ejemplo más de cómo usarlo, esta vez con MongoDB. Veremos dos ejemplos, uno más básico y otro configurando autenticación. 

Sabemos que una de las cosas más interesantes de Testcontainers es la posibilidad de completar tus pruebas unitarias y emular un entorno de integración. Facilitando nuestro desarrollo y minimizando posibles errores en el ciclo de vida de la aplicación. 

En este primer ejemplo, simplemente crearemos una instancia de MongoDB sin ningún tipo de configuración. Lo podremos hacer con ayuda de la clase MongoDBContainer a la que solo deberemos indicar que versión de MongoDB queremos usar. Si queremos saber la cadena de conexión la podremos obtener de la propia instancia de MongoDBContainer a través del método 'getConnectionString'

Para la prueba de concepto hemos usado el propio cliente de Mongo que nos permita leer y escribir en la base de datos. 

@Log4j2
public class TestContainerMongoDBTest {
  static final String DBNAME = "myDatabase";
  static String URI;
  static MongoDBContainer mongoDBContainer;
  @BeforeClass
  public static void startMongoDB() {
    mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:7.0.14"));
    mongoDBContainer.start();
    URI = mongoDBContainer.getConnectionString() + "/" + DBNAME;
  }
  @Test
  public void insertAndCount() {
    try(MongoClient mongoClient = MongoClients.create(URI)){
      MongoDatabase database = mongoClient.getDatabase(DBNAME);
      MongoCollection<Document> collection = database.getCollection("testCollection");
      Document doc1 = new Document("title", "Inception").append("imdb", new Document("rating", 8.8));
      collection.insertOne(doc1);
      assertThat(collection.countDocuments(), equalTo(1L));
    }
  }
}

Como hemos visto, en un par de lineas podemos tener una MongoDB lista para realizar nuestras pruebas. Pero puede que necesitemos añadir más configuración a la creación de la instancia de MongoDB. En ese caso, es mejor utilizar la clase GenericContainer de Testcontainers. La cual nos permite un mayor grado de configuración. Por ejemplo en este caso, indicaremos que puerto expondremos, cual será el usuario y la contraseña a utilizar, incluso el nombre del esquema a usar de la base de datos. 

Por lo demás el ejemplo será igual al anterior y por tanto lo podremos volver a probar con el cliente de MongoDB. 

public class MongoDBCustomTest {
  static final String DBNAME = "myDatabase";
  static final String USER = "mongoadministrator";
  static final String PASS = "secret";
  static final int MONGO_PORT = 27017;
  static GenericContainer mongoDBContainer;
  static String URI;
  @BeforeClass
public static void startMongoDB() { mongoDBContainer = new GenericContainer(DockerImageName.parse("mongo:7.0.14")); List<String> list = new ArrayList<>(); list.add("MONGO_INITDB_ROOT_USERNAME="+USER); list.add("MONGO_INITDB_ROOT_PASSWORD="+PASS); list.add("MONGO_INITDB_DATABASE="+DBNAME); mongoDBContainer.setEnv(list); mongoDBContainer.withExposedPorts(MONGO_PORT); mongoDBContainer.start(); URI = String.format("mongodb://%s:%s@%s:%d/%s?authSource=admin",
        USER, PASS, mongoDBContainer.getHost(), 
        mongoDBContainer.getFirstMappedPort(), DBNAME);
  }
  @Test
  public void insertAndCount() {
    try (MongoClient mongoClient = MongoClients.create( new ConnectionString(URI))) {
      MongoDatabase database = mongoClient.getDatabase(DBNAME);
      MongoCollection<Document> collection = database.getCollection("testCollection");
      Document doc1 = new Document("title", "Inception").append("imdb", new Document("rating", 8.8));
      collection.insertOne(doc1);
      assertThat(collection.countDocuments(), equalTo(1L));
    }
  }
}

Como siempre, espero que os haya servido de ayuda y aquí puedes ver todo el código.

domingo, 7 de enero de 2024

Wiremock y Quarkus: Como configurarlo

Wiremock es una gran herramienta que puede ayudarte en la realización de pruebas de calidad de tu código. Un paso fundamental a la hora de desarrollar pruebas de aplicaciones o servicios que invocan a otros. Las pruebas son un paso fundamental en el desarrollo de software. Y Quarkus es un framework Java diseñado especialmente para el desarrollo de aplicaciones en contenedores, sin servidor y en la nube, especialmente para Kubernetes. De ambos hemos hablado varias veces ya.

Hoy veremos cómo podemos usarlos juntos, puesto que su uso no es tan sencillo, como pudiera ser con otra aplicación realizada con otro framework Java. Y para lograrlo haremos uso de la anotación @QuarkusTestResource. En una clase de testing debemos añadir dicha anotación asociada a una clase que implemente a su vez la interfaz QuarkusTestResourceLifecycleManager. Esto nos permitirá inicializar un servicio externo antes de la ejecución de las propias pruebas. En nuestro caso, nos permitirá arrancar el servidor Wiremock.

public class WiremockTestResource implements QuarkusTestResourceLifecycleManager {
  public static WireMockServer wireMockServer;
  @Override
  public Map<String, String> start() {
    wireMockServer = new WireMockServer(57001);
    wireMockServer.start();
    return new HashMap<String, String>();
  }
  @Override
  public void stop() {
    if (wireMockServer != null) {
      wireMockServer.stop();
    }
  }
}

Una vez creada la clase que permite arrancar Wiremock, haremos uso de ella a través de la anotación @QuarkusTestResource. De esta forma ya solo nos quedará indicar los stubs asociados a nuestros tests.

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class) //Resource that call external service
@QuarkusTestResource(WiremockTestResource.class)
public class WiremockResourceBasicTest {
  @Test
  public void getById() {
    Log.info("WiremockResourceBasicTest - getById");
    ObjectMapper mapper = new ObjectMapper();
    Movie movie = new Movie(1L, "Denis Villeneuve", "Dune");
    Log.info("WM port: " + WiremockTestResource.wireMockServer.getOptions().portNumber());
    WiremockTestResource.wireMockServer.stubFor(
      WireMock.get("/imdb/film/1").willReturn(WireMock.aResponse()
      .withJsonBody(mapper.valueToTree(movie))
      .withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, "application/json")));
    when().get("/1").andReturn().then().statusCode(200)
      .body("id", is(1)).body("name", is("Dune")).body("director",
      is("Denis Villeneuve"));
  }
}

Como veis es muy sencillo. Ahora veremos una opción un poco más complejas pero que permitirá crear el servidor de Wiremock con más opciones de configuración y modificarlas dinámicamente. Aunque el desarrollo es un poco lioso, debido a que tiene referencias cruzadas. 

Por un lado crearemos una anotación que tenga las propiedades que queramos modificar dinámicamente posteriormente a través de la propia anotación. Pero también le añadiremos la anotación @QuarkusTestResource que hará referencia a la clase que controla el servidor de Wiremock. 

@QuarkusTestResource(value = WiremockResourceConfigurable.class, restrictToAnnotatedClass = true)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WiremockTestAnnotation {
    String port() default "57005";
}

Ahora mostraremos la clase que configura el servidor de Wiremock. Será similar al ejemplo anterior, pero con tres añadidos. Para empezar, la interfaz que vamos a implementar será QuarkusTestResourceConfigurableLifecycleManager, y está estará asociada a la anotación creada anteriormente. Luego, a través de los atributos de la anotación, en el método init, podremos configurar dinámicamente el servidor. Y por último, con el retorno del método start, también podremos sobre escribir valores del fichero application.properties con los valores dinámicos. En este caso, a través de esta sobre escritura, podremos modificar el puerto del servicio externo pre configurado para el cliente.

public class WiremockResourceConfigurable 
  implements QuarkusTestResourceConfigurableLifecycleManager<WiremockTestAnnotation> {	
    public static WireMockServer server;
    private String port;
    @Override
    public void init(WiremockTestAnnotation params) {
        port = params.port();
    }
    @Override
    public Map<String, String> start() {
	server = new WireMockServer(Integer.valueOf(port));
        server.start();
	return  Map.of(
          "quarkus.rest-client.\"com.home.example.service.ExternalService\".url", 
          "http://localhost:"+port
          );
    }
    //...
}

Ya solo nos quedaría mostrar el test, haciendo uso de la anotación que nos permitiría modificar el puerto del servidor wiremock y del servicio externo dinámicamente. Muy similar al anterior. 

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class)
@WiremockTestAnnotation(port = "57005")
public class WiremockResourceTest {
    @Inject
    @ConfigProperty(name = "quarkus.rest-client.\"com.home.example.service.ExternalService\".url")
    private String serverUrl;
    @Test
    public void getById() {
        Log.info("serverUrl: " + serverUrl);
        ObjectMapper mapper = new ObjectMapper();
        Movie movie = new Movie(1L, "Denis Villeneuve", "Dune");
        WiremockResourceConfigurable.server.stubFor(
            WireMock.get("/imdb/film/1").willReturn(WireMock.aResponse().withJsonBody(mapper.valueToTree(movie))
	    .withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, "application/json")));	
        when().get("/1").andReturn().then().statusCode(200)
            .body("id", is(1)).body("name", is("Dune")).body("director",is("Denis Villeneuve"));
    }
}

Con esto hemos visto una particularidad más para mejorar los test en Quarkus. Y aunque Wiremock es una gran herramienta que desde hace tiempo es muy util para el desarrollo de pruebas. No quisiera omitir, que también es posible mockear el servicio externo de una forma sencilla con Quarkus.  Y esto es posible haciendo uso de las anotaciones @InjectMock y @RestClient. En el ejemplo que mostramos a continuación, ExternalService ha sido todo el tiempo el cliente web que hemos mockeado con Wiremock.  

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class)
public class WiremockResourceInjectTest {
    @InjectMock
    @RestClient
    ExternalService extService;
    @Test
    public void getById() {
        Mockito.when(extService.getMovieById(Mockito.anyLong())).thenReturn(new Movie(1L, "Denis Villeneuve", "Dune"));
        when().get("/1").andReturn().then().statusCode(200).body("id", is(1)).body("name", is("Dune")).body("director",
            is("Denis Villeneuve"));
    }
}

Espero que como siempre haya sido util, y si quieres puedes ver el código aquí. 

viernes, 12 de mayo de 2023

Testcontainers: Uso básico

Ya hace 3 años, sobre Agosto de 2020 vi un post sobre cómo realizar pruebas unitarias con contenedores. Y aunque tuve la intención de hacer un post, nunca llegue a hacerlo. Pero ahora tras ver cómo funcionan las pruebas con Quarkus, del cual intentaré hablar más adelante, me he interesado en sacar este post. 

En múltiples de los posts que he realizado, he utilizado siempre contenedores para poder realizar las pruebas, y poder comprobar el ejemplo práctico. Normalmente dejando un docker-compose.yml que permitiese levantar y configurar los sistemas externos con los cuales iba a trabajar la aplicación. 

Ahora vamos a hacer algo parecido Y para llevarlo a cabo utilizaremos Testcontainers. Es una librería Java que nos permite crear instancias de Docker y manejarlas en base a nuestro interés. En el ejemplo utilizaremos las siguientes librerías:

  • org.testcontainers:testcontainers:1.16.0
  • org.testcontainers:junit-jupiter:1.16.0
  • org.testcontainers:mysql:1.16.0
  • org.apache.camel:camel-test-spring-junit5:3.16.0
  • org.springframework.boot:spring-boot-starter-test:2.5.1

Para empezarla a utilizar, usaremos principalmente dos anotaciones:

  • @Testcontainers: que nos permite utilizar Testcontainers con JUnit 5.
  • @Containers: Nos permite indicar a las instancias de contenedores que queremos que sean gestionadas por Testcontainers. Aunque como veremos más adelante podemos utilizar Testcontainers sin ella. 
Antes de empezar debemos aclarar un poco el funcionamiento. Testcontainers nos permite levantar gestionar contenedores de cualquier imagen que queramos. Pudiéndole añadir a través de sus métodos una breve configuración. Pero también tiene módulos específicos para hacer uso de contenedores comunes, como puede ser de BBDD, Hashicorp, RabitMQ o Kafka.

Otro asunto importante a tener en cuenta, que aunque nosotros les indiquemos los puertos que queremos exponer, no podremos crear bindings tal y como se realiza en un docker-compose. Sino que los puertos expuestos serán enlazados con puertos aleatorios de nuestra máquina. Debido a esto, deberemos modificar antes de realizar las pruebas la configuración que tenemos por defecto. Para que podamos indicar los puertos que utilizarán los sistemas, una vez han sido arrancados con Testcontainers. 

Para ver cómo funciona, utilizaremos ejemplos hechos con Apache Camel y Spring Boot. En el primer ejemplo enviaremos y recibiremos mensajes de una cola de ActiveMQ. A continuación detallo los componentes más importantes del ejemplo:
  • La instancia de GenericContainer nos permitirá indicar la imagen de ActiveMQ a crear y los puertos a exponer.
  • Crear un método BeforeAll que permita arrancar el contenedor y el puerto vinculado externo. 
  • Modificar la configuración de los componentes que realizan la comunicación con el sistema externo. En Spring Boot lo podemos hacer fácilmente con un método anotado con @DynamicPropertySource.
@Testcontainers
@CamelSpringBootTest
@SpringBootTest(classes = ApacheCamelTestApplication.class)
@Log4j2
public class ApacheActiveMqRouterTest {
    @Autowired
    ProducerTemplate producer;
    @Container
    private static GenericContainer container = new GenericContainer("rmohr/activemq").withExposedPorts(61616, 8161);
    private static Integer tcpPort;
    @BeforeAll
    public static void beforeAll() {
        container.start();
        tcpPort = container.getMappedPort(61616);
    }
    @DynamicPropertySource
    static void replaceProperties(DynamicPropertyRegistry registry) {
        registry.add("activemq.broker-url", () -> "tcp://localhost:" + tcpPort);
    }
    @Test
    public void amqTo01() throws InterruptedException {
        producer.sendBody("direct:SendToPublic", "mensaje ");
    }
}

En el segundo ejemplo, será similar en funcionamiento al anterior. Con la excepción de qué utilizaremos un módulo concreto de Testcontainers. Con él, a través de la clase MySQLContainer podremos hacer configuraciones específicas para crear el contenedor. Como indicar un script para inicializar la base de datos. 

@Testcontainers
@CamelSpringBootTest
@SpringBootTest(classes = {ApacheCamelTestApplication.class})
@Slf4j
public class ApacheMySQLRouterTest{
    static final DockerImageName MYSQL_57_IMAGE = DockerImageName.parse("mysql:5.7.34");
    static MySQLContainer<?> database = new MySQLContainer<>(MYSQL_57_IMAGE)
    .withInitScript("scripts/init_mysql.sql")
    .withDatabaseName("library").withLogConsumer(new Slf4jLogConsumer(log));
    @Autowired
    ProducerTemplate producer;
@BeforeAll public static void beforeAll() throws IOException { database.start(); } @DynamicPropertySource static void databaseProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", database::getJdbcUrl); registry.add("spring.datasource.username", database::getUsername); registry.add("spring.datasource.password", database::getPassword); } @Test public void getBookById() throws InterruptedException { producer.sendBodyAndHeader("direct:getBookById", null, "id", 1); } }

Antes de terminar dos cosas. El primero, es que durante la realización de los ejemplos he tenido problemas con distintas versiones de Testcontainers: 
  • 1.18: Genero la excepción NoSuchMethodError asociada al método optionallyMapResourceParameterAsVolume. 
  • 1.17: Genero ClassNotFoundException: asociado a la clase org.testcontainers.shaded.org.apache.commons.lang.StringUtils
Y lo segundo, es que sí debemos utilizar un varios contenedores como es en este caso podemos tener errores debido a que nuestras aplicación se intente conectar a un sistema externo y no tengamos su contenedor levantado. Por tanto como recomendación, podemos crear una clase que contenga toda la configuración de los contenedores y de la que extenderemos. 

@SpringBootTest
@Testcontainers
@CamelSpringBootTest
@Slf4j
public class TestcontainersConf {
    public static final DockerImageName MYSQL_57_IMAGE = DockerImageName.parse("mysql:5.7.34");
    static GenericContainer<?> container = new GenericContainer<>("rmohr/activemq").withExposedPorts(61616, 8161);    
    static MySQLContainer<?> database = new MySQLContainer<>(MYSQL_57_IMAGE)
    .withInitScript("scripts/init_mysql.sql")
    .withDatabaseName("library").withLogConsumer(new Slf4jLogConsumer(log));
    @BeforeAll
    public static void beforeAll() {
        container.start();
        database.start();
    }
    @DynamicPropertySource
    static void replaceProperties(DynamicPropertyRegistry registry) {
        registry.add("activemq.broker-url", () -> "tcp://localhost:" + container.getMappedPort(61616));
        registry.add("spring.datasource.url", database::getJdbcUrl);
        registry.add("spring.datasource.username", database::getUsername);
        registry.add("spring.datasource.password", database::getPassword);
    }
}
// ............................
public class ApacheMySQLRouterTest extends TestcontainersConf{
    @Autowired
    ProducerTemplate producer;
    @Test
    public void getBookById() throws InterruptedException {
        producer.sendBodyAndHeader("direct:getBookById", null, "id", 1);
    }
}

Espero que os haya ayudado, y si os interesa, aquí tenéis todo el código fuente. 

lunes, 13 de febrero de 2023

Wiremock y JUnit5

Hoy vamos a ver un sencillo post sobre cómo configurar Wiremock con JUnit 5. Algo sencillo pero que cambia respecto a cómo era con JUnit 4 rules. Para nuestro ejemplo utilizaremos las siguientes versiones:

  • junit-jupiter-api:5.9.2
  • wiremock-jre8:2.35.0

Para empezar, si queremos utilizar Wiremock de forma muy básica, simplemente nos bastará con la anotación @WireMockTest. La cual nos permitirá modificar lo siguiente:

  • httpPort: Para indicar en que puerto podremos hacer llamadas HTTP.
  • httpsEnabled y httpsPort. Para indicar que queremos hacer llamadas HTTPS y en que puerto. 
  • proxyMode. Para emular un nombre de dominio distinto a localhost. En dicho caso y usando HTTPClient deberemos usar el método useSystemProperties a la hora de crear el cliente. 
A continuación podemos ver un ejemplo de un invocación a un nombre de dominio distinto a localhost y HTTPS. Para este ultimo ya no necesitaremos crear un certificado autoafirmado como lo hacía antiguamente. 

@Log4j2
@WireMockTest(httpsEnabled = true, httpsPort = 9090, proxyMode = true)
public class WiremockBasicTest {
  private static final String BEARER_TOKEN = "Bearer 77d9b8f0-fafe-3778-addf-2755bdc53c88";
  private static final String JSON_CONTENT = "{\"hellow\":\"world\"}";

  @Test
  public void doGetAndGetResponse_proxyMode() throws Exception{
    String sEndpoint = "https://mydomain.com:9090/sample";
    Map<String, String> headers = new HashMap<>();
    headers.put(HttpHeaders.AUTHORIZATION, BEARER_TOKEN);
    stubFor(get("/sample").withHeader(HttpHeaders.AUTHORIZATION, WireMock.equalTo(BEARER_TOKEN))
        .withHost(WireMock.equalTo("mydomain.com"))
        .willReturn(aResponse().withBody(JSON_CONTENT).withStatus(200)));

    String body = null;
    HttpGet get = new HttpGet(sEndpoint);
    get.setHeaders(headers.entrySet().stream().map(entry -> new BasicHeader(entry.getKey(), entry.getValue())).toArray(Header[]::new));
    try (CloseableHttpClient httpClient = createAcceptSelfSignedCertificateClient(); CloseableHttpResponse response = httpClient.execute(get)) {
        body = EntityUtils.toString(response.getEntity(), Charset.defaultCharset());
    }
    assertThat(body, equalTo(JSON_CONTENT));
  }
}

Y aunque con muy poco ya podemos hacer mucho. Puede que haya casos en los que necesitemos un poco más de configuración. Realizar las invocaciones como hacíamos antes con los JUnit 4 rules y tener acceso al método wireMockConfig. 

Esto lo podremos hacer a través de las extensiones de JUnit5. Pero la instancia que creemos será la misma que debemos utilizar para crear los distintos stub. Lo beneficioso de este enfoque, es que además nos permite crear distintas instancias de la extensión y usar ambas. 

public class WiremockComplexTest {
  private static final String BEARER_TOKEN = "Bearer 77d9b8f0-fafe-3778-addf-2755bdc53c88";
  private static final String JSON_CONTENT = "{\"hellow\":\"world\"}";

  @RegisterExtension
  static WireMockExtension wme = WireMockExtension.newInstance()
      .options(wireMockConfig().httpsPort(9090).port(8085)
      .notifier(new ConsoleNotifier(true))).proxyMode(true).build();

  @Test
  public void doGetAndGetResponse_proxyMode() throws Exception {
    String sEndpoint = "https://mydomain.com:9090/sample";
    Map<String, String> headers = new HashMap<>();
    headers.put(HttpHeaders.AUTHORIZATION, BEARER_TOKEN);
    wme.stubFor(get("/sample").withHeader(HttpHeaders.AUTHORIZATION, WireMock.equalTo(BEARER_TOKEN)).withHost(WireMock.equalTo("mydomain.com"))
        .willReturn(aResponse().withBody(JSON_CONTENT).withStatus(200)));

    String body = null;
    HttpGet get = new HttpGet(sEndpoint);
    get.setHeaders(headers.entrySet().stream().map(entry -> new BasicHeader(entry.getKey(), entry.getValue())).toArray(Header[]::new));
    try (CloseableHttpClient httpClient = createAcceptSelfSignedCertificateClient(); CloseableHttpResponse response = httpClient.execute(get)) {
      body = EntityUtils.toString(response.getEntity(), Charset.defaultCharset());
    }
    assertThat(body, equalTo(JSON_CONTENT));
  }
}

Por último, si este enfoque es el ideal para tus pruebas. Pero no quieres estar indicando la instancia en todos los métodos de WireMock. Podemos indicar que la instancia la cree de forma estática y ya si que sería de la misma forma que cuando usábamos los JUnit 4 rules. Para ello solo tendríamos que utilizar el método configureStaticDsl(true)

@RegisterExtension
static WireMockExtension wme = WireMockExtension.newInstance()
    .options(wireMockConfig().httpsPort(9090).port(8085)
    .notifier(new ConsoleNotifier(true)))
    .configureStaticDsl(true).proxyMode(true).build();

Espero que este post haya sido útil y os ayude en la actualización de vuestro software de pruebas. 

domingo, 23 de enero de 2022

Apache Camel: Mock & Testing

En este post vamos a ver como realizar pruebas de los route que desarrollamos con Apache Camel. Además veremos varias herramientas y buenas prácticas que podemos utilizar para mejorar no solo nuestros tests sino también nuestro código. A tener en cuenta que haremos uso de JUnit 5 para realizar las pruebas. 

Para empezar comenzaremos con la librería necesaria para realizar los tests, al realizar nuestra aplicación con Spring Boot, necesitamos las dependencias del testing:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
	<exclusions>
		<exclusion>
			<groupId>org.junit.vintage</groupId>
			<artifactId>junit-vintage-engine</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
	<groupId>org.apache.camel</groupId>
	<artifactId>camel-test-spring-junit5</artifactId>
	<scope>test</scope>
</dependency>

Lo siguiente será crear nuestro test. Y tal como hemos indicado, Apache Camel nos proporcionará anotaciones y utilidades que nos permitirán llevar a cabo nuestros tests. A continuación indicaremos algunos:

  • @CamelSpringBootTest: Anotación necesaria para testear aplicaciones de Apache Camel con Spring Boot.
  • @EnableAutoConfiguration: Permitirá utilizar el contexto de Spring con una configuración automática, además de habilitar el contexto de Apache Camel. 
  • @SpringBootTest: Permite configurar el contexto de Spring y además indicar propiedades de dicho contexto específicas para el test.
  • ProducerTemplate: Interface que permite el intercambio de mensajes a endpoints de diferentes formas. Los métodos de tipo send son solo de entrada y los request de entrada/salida. 
  • Mock: Endpoint específico de testing que además permite hacer comprobaciones sobre los mensajes que le llegan: número de mensajes, contenido, etc.
  • Direct: Stub que permite invocar routes de forma directa sin la necesidad de invocarlas a través de un protocolo de acceso específico.
  • Seda: Stub que simula una cola asíncrona y que permite varios suscriptores. 
@CamelSpringBootTest
@EnableAutoConfiguration
@SpringBootTest
class MoreSimpleMockTest {
  @Configuration
  static class TestConfig {
	@Bean
	RoutesBuilder route() {
		return new RouteBuilder() {
			@Override
			public void configure() throws Exception {
				from("direct:directTest").log("${body}")
						.transform(simple("Hello ${body}"))
						.to("mock:testA");
				from("seda:next?multipleConsumers=true").routeId("testA").log("${body}")
						.transform(simple("Hello ${body}"))
						.to("mock:testA");
				from("seda:next?multipleConsumers=true").routeId("testB").to("mock:testB");
			}
		};
	}
  }
	@Autowired
	ProducerTemplate producerTemplate;

	@EndpointInject("mock:testA")
	MockEndpoint mockAEndpoint;

	@EndpointInject("mock:testB")
	MockEndpoint mockBEndpoint;

	@Test
	public void requestDirectTest() throws InterruptedException {
		String ret = producerTemplate.requestBody("direct:directTest", "World", String.class);
		assertEquals("Hello World", ret);
	}
	@Test
	public void sendDirectTest() throws InterruptedException {
		mockAEndpoint.expectedHeaderReceived("Content-Type", "text/plain");
		producerTemplate.sendBodyAndHeader("direct:directTest", "World", "Content-Type", "text/plain");
		mockAEndpoint.assertIsSatisfied();
	}
@Test public void sedaTest() throws InterruptedException { mockAEndpoint.setExpectedMessageCount(1); mockAEndpoint.expectedBodiesReceived("Hello World"); mockBEndpoint.setExpectedMessageCount(1); mockBEndpoint.expectedBodiesReceived("World"); producerTemplate.sendBodyAndHeader("seda:next", "World", "Content-Type", "text/plain"); mockAEndpoint.assertIsSatisfied(); mockBEndpoint.assertIsSatisfied(); } }

Como hemos visto en el ejemplo anterior, con estos siete elementos podremos realizar muchos tipos de pruebas y mejorar la cobertura de nuestros tests en aplicaciones de Apache Camel. Pero ahora veremos como testear no simplemente mocks o routes realizadas ad-hoc para nuestras pruebas, sino aquellas rutas que desarrollamos y serán desplegadas. Por ejemplo, la siguiente route:

@Component
public class BookMockRouter extends RouteBuilder {

	private static Map<Integer, Book> books = new HashMap<>();

	static {
		books.put(1, new Book(1, "Dune", "Frank Herbert"));
		books.put(2, new Book(2, "The stars my destination", "Alfred Bester"));
		books.put(3, new Book(3, "Ender's game", "Orson S. Card"));
	}
	@Override
	public void configure() throws Exception {
		rest().get("book").produces(MediaType.APPLICATION_JSON_VALUE)
				.route().routeId("mockClientGetAll")
				.bean(BookMockRouter.class, "getAll(})").marshal().json();
	}
	public Collection<Book> getAll() {
		return books.values();
	}
}

Si tratamos testear este endpoint necesitaríamos realizar invocaciones de tipo HTTP, realizando pruebas no solo de la lógica sino también de cómo realizamos la conexión al endpoint. Dejando de ser pruebas unitarias y pasando a ser pruebas de integración. Para poder realizar los test unitarios, es una buena práctica poder dividir la lógica en elementos que nos permita validar la lógica más fácilmente. Por tanto nuestra route, quedaría de la siguiente forma:  

rest().get("book").produces(MediaType.APPLICATION_JSON_VALUE).to("direct:restGetAllBooks");
from("direct:restGetAllBooks").bean(BookMockRouter.class, "getAll(})").marshal().json();

Una vez realizada la refactorización podremos realizar las pruebas de esta nueva route. Pero para ello también deberemos configurar la ruta en el test, de la siguiente forma:

@Configuration
static class TestConfig {
	@Bean
	RoutesBuilder route() {
		return new BookMockRouter();
	}
}
@Autowired
ProducerTemplate producerTemplate;
@Test
public void requestBody() {
	producerTemplate.sendBody("direct:restGetAllBooks", "");
	final String ret = producerTemplate.requestBody("direct:restGetAllBooks", "", String.class);
	assertEquals("[{\"id\":1,\"name\":\"Dune\",\"author\":\"Frank Herbert\"},{\"id\":2,\"name\":\"The stars my destination\",\"author\":\"Alfred Bester\"},{\"id\":3,\"name\":\"Ender's game\",\"author\":\"Orson S. Card\"}]",
			ret);
}

Por último veremos una buena práctica para nuestros desarrollos y que nos pueden ayudar también a la hora de realizar las pruebas. Y esta es la parametrización de nuestros endpoints. Para ello tendríamos que realizar tres cambios en nuestro código, los cuales serán aplicados en un nuevo endpoint. 

El primer cambio es crear el endpoint al cual se acceda a través de una variable. 

@Override
public void configure() throws Exception {
	rest().get("book").produces(MediaType.APPLICATION_JSON_VALUE).to("direct:restGetAllBooks");
	from("direct:restGetAllBooks").bean(BookMockRouter.class, "getAll(})").marshal().json();

	rest().get("book/{id}").produces(MediaType.APPLICATION_JSON_VALUE).to("direct:restGetById");
	from("direct:restGetById").log("From rest.get.book.id").bean(BookMockRouter.class, "getById(${header.id})")
			.marshal().json();

	from("{{rest.get.libro.id}}").log("From rest.get.libro.id").bean(BookMockRouter.class, "getById(${header.id})")
			.marshal().json();
}

El segundo cambio, es indicar en el fichero application.properties el valor de dicha variable. En este caso será fija en todos los entornos, pero el uso de variables nos posibilita el desarrollo en el cual los endpoints de entrada o salida sean dinámicos. 

# Routes
rest.get.libro.id=rest:GET:/libro/{id}

Por último, en nuestro fichero de pruebas indicamos que para llevarlas a cabo, ese endpoint dinámico no se invocará a través de HTTP sino del componente Direct. Esto lo podemos hacer a través de la anotación @SpringBootTest. El resto del test será similar a lo que ya tenemos. 

@CamelSpringBootTest
@EnableAutoConfiguration
@SpringBootTest(properties = { "rest.get.libro.id = direct:start" })
class BookMockRouteTest {
	@Configuration
	static class TestConfig {
		@Bean
		RoutesBuilder route() {
			return new BookMockRouter();
		}
	}

	@Autowired
	ProducerTemplate producerTemplate;
	@Test
	public void variableRestGetById() {
		final String ret = producerTemplate.requestBodyAndHeader("direct:start", "", "id", "1", String.class);
		assertEquals("{\"id\":1,\"name\":\"Dune\",\"author\":\"Frank Herbert\"}", ret);
	}
}

Una alternativa al uso de variables, puede ser utilizar el componente adviceWith. Pero creo que tanto a nivel de desarrollo como de pruebas es mejor el uso de las variables. 

Espero que os haya servido y os ayude en la mejora del testing de Apache Camel. Como siempre, el código fuente lo tenéis aquí.