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í

No hay comentarios:

Publicar un comentario