En los procesos de transformación digital de cualquier empresa, la migración de las arquitecturas monolíticas a arquitecturas basadas en microservicios es prácticamente una obligación. Como ya hemos comentado en otros posts, los microservicios ofrecen múltiples ventajas como escalado, integración con otros sistemas, simplicitud, etc. Pero también obliga a los desarrolladores a que su código sea más sólido y de mayor calidad, lo que obliga a elaborar tests de gran calidad para asegurar esta solidez.

Para desarrollar microservicios usando Java como lenguaje de programación, Spring, con su módulo SpringBoot, facilita mucho la tarea de implementar dicho microservicio. Si además, debe integrarse con una base de datos NoSQL como puede ser MongoDB, Spring también ofrece módulos que facilitan esta integración.

Una vez tenemos implementado nuestro microservicio SpringBoot con su base de datos se recomienda utilizar una base de datos en memoria para poner a prueba todo el flujo a la hora de diseñar los test integrados, desde la llamada REST a la lógica de base de datos.

Para implementar tests integrados en servicios SpringBoot con MongoDB, una posible solución es utilizar Fongo. Y de ello vamos a hablar en este post.

Fongo permite cargar una base de datos MongoDB en memoria para ser usada en los tests integrados. La última versión hasta la fecha del módulo Spring Data MongoDB (versión 1.9) ha añadido la compatibilidad con MongDB 3.0 y con el Driver de Java para MongoDB versión 3.2.

En primer lugar iremos a la página de Spring Initializer para generar el esqueleto de nuestro microservicio con SpringBoot:

Seleccionamos las dependencias de Web y MongoDB y haremos clic en Generate project para descargar el esqueleto, que importaremos en como proyecto Maven en el IDE que usemos.

Una vez importado el proyecto, la estructura que tendremos será la siguiente:

Para este ejemplo, hemos usado la versión 1.4.2 de SpringBoot. Incluyendo el starter de MongoDB nos incluye automáticamente la versión 3.2.2 del driver de MongoDB para Java. Para dicha versión, debemos usar la versión 2.0.x de Fongo.

Nuestro microservicio de ejemplo tendrá dos operaciones: Una para leer los datos de una persona por un id y otra para grabar los datos de una nueva persona. El bean Persona tendrá los siguientes datos:


import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "person")
public class Person {
 @Id
 private int id;
 private String name;
 private String address;
public void setId(int id) {
 this.id = id;
 }
public int getId(){
 return id;
 }
 public String getName() {
 return name;
 }
 public void setName(String name) {
 this.name = name;
 }
 public String getAddress() {
 return address;
 }
 public void setAddress(String address) {
 this.address = address;
 }
}

Una vez tenemos el bean de datos, en la capa de acceso a datos, en lugar de crear una interfaz que extiende de la clase MongoRepository de spring-data-mongodb, hemos creado una interfaz para nuestra capa de acceso a datos con los métodos necesarios y una implementación de esa interfaz con una clase MongoOperation inyectada por constructor:


import com.isolisduran.springboot.bean.Person;
public interface PersonRepository {
 Person findOne(int id);
 Person save(Person person);
}


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Repository;
import com.isolisduran.springboot.bean.Person;
import com.isolisduran.springboot.repository.PersonRepository;
@Repository
public class PersonRepositoryMongo implements PersonRepository{
 private final MongoOperations mongoOperations;
 @Autowired
 public PersonRepositoryMongo(MongoOperations mongoOperations) {
        this.mongoOperations = mongoOperations;
 }
 @Override
 public Person findOne(int id) {
 return mongoOperations.findOne(new Query(Criteria.where("id").is(id)), Person.class);
 }
 @Override
 public Person save(Person person) {
 mongoOperations.save(person);
 return person;
 }
}

La capa de lógica de negocio quedaría de la siguiente forma:


import com.isolisduran.springboot.bean.Person;
public interface PersonService {
 public Person getById(int id);
 public Person create(Person person);
}


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.isolisduran.springboot.bean.Person;
import com.isolisduran.springboot.repository.PersonRepository;
import com.isolisduran.springboot.service.PersonService;
@Service
public class PersonServiceDefault implements PersonService {
 @Autowired
 private PersonRepository personRepository;
 @Override
 public Person getById(int id) {
 return personRepository.findOne(id);
 }
 @Override
 public Person create(Person person) {
 return personRepository.save(person);
 }
}

Y el Controller que expone el API REST quedaría de la siguiente forma:


import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.isolisduran.springboot.bean.Person;
import com.isolisduran.springboot.service.PersonService;
@RequestMapping("/api")
@RestController
public class PersonController {
 @Autowired
 private PersonService personService;
 @RequestMapping(method = RequestMethod.GET, value = "/person/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
 public @ResponseBody ResponseEntity getById(@PathVariable int id) {
 Person person = personService.getById(id);
 return new ResponseEntity(person, HttpStatus.OK);
 }
 @RequestMapping(method = RequestMethod.POST, value = "/person", produces = MediaType.TEXT_PLAIN_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
 public @ResponseBody ResponseEntity create(@Valid @RequestBody Person person) {
 Person newPerson = personService.create(person);
 return new ResponseEntity(String.valueOf(newPerson.getId()), HttpStatus.CREATED);
 }
}

Con este código tendríamos microservicio implementado con Java+SpringBoot que usa una base de datos MongoDB y que podemos compilar y testear ejecutando desde línea de comandos la instrucción:


$: mvn clean package

Veremos que además de compilar, también ejecuta un test que prueba que el microservicio arranca correctamente. Veremos en el log que aunque el test no falla, muestra un error del tipo:


Exception in monitor thread while connecting to server localhost:27017

Esto se debe a que por defecto el driver de Mongo intenta establecer en el arranque una conexión a una base de datos local. Por ahora lo podemos obviar y borrar la clase del paquete de test, pero para entornos productivos deberíamos excluir la autoconfiguración de Mongo y cargar manualmente la configuración de MongoDB en una clase de configuración.

Para incluir Fongo en nuestro proyecto, podemos incluir la dependencia de Fongo en el pom.xml de nuestro proyecto Maven de la siguiente forma:


<dependency>
   <groupId>com.github.fakemongo</groupId>
   <artifactId>fongo</artifactId>
   <version>2.0.6</version>
   <scope>test</scope>
</dependency>

Una vez incluida la dependencia de Fongo, procedemos a implementar nuestros tests integrados. En este caso, probaremos las dos operaciones de nuestro API REST. El objetivo es probar esta llamada mockeando la base de datos usando Fongo.

Los pasos son los siguientes:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import com.github.fakemongo.Fongo;
import com.mongodb.Mongo;
public abstract class AbstractFongoBaseConfiguration extends AbstractMongoConfiguration{
    @Autowired
    private Environment env;
    @Override
    protected String getDatabaseName() {
        return env.getRequiredProperty("spring.data.mongodb.database");
    }
    @Override
    public Mongo mongo() throws Exception {
        return new Fongo(getDatabaseName()).getMongo();
    }
}


import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration(exclude = { EmbeddedMongoAutoConfiguration.class, MongoAutoConfiguration.class,
 MongoDataAutoConfiguration.class })
@Configuration
@ComponentScan(basePackages = { "com.isolisduran.springboot" }, excludeFilters = {
 @ComponentScan.Filter(classes = { SpringBootApplication.class }) })
public class ConfigServerWithFongoConfiguration extends AbstractFongoBaseConfiguration {
}

Con esto podemos proceder a implementar nuestro test integrado que arrancará el contexto de Spring para poder llamar a nuestro API REST y comprobar el flujo completo.


@SpringBootTest(classes = { ConfigServerWithFongoConfiguration.class})


@TestPropertySource(properties = {"spring.data.mongodb.database=test"})

Con estos dos puntos tenemos configurada la Fongo para simular una base de datos MongoDB.

Una vez hecho esto, si necesitamos cargar datos específicos para realizar el test integrado, podemos usar la clase MongoTemplate para crear colecciones y documentos:


@Autowired
private MongoTemplate mongoTemplate;


mongoTemplate.createCollection("collection-name");
mongoTemplate.insert(objectToInsert);

Con todo esto, un ejemplo de test integrado completo podría ser:


import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.isolisduran.springboot.bean.Person;
import itest.config.ConfigServerWithFongoConfiguration;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { ConfigServerWithFongoConfiguration.class }, properties = {
 "server.port=8980" }, webEnvironment = WebEnvironment.DEFINED_PORT)
@AutoConfigureMockMvc
@TestPropertySource(properties = { "spring.data.mongodb.database=test" })
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class PersonControllerTest {
 @Autowired
 private MongoTemplate mongoTemplate;
 @Autowired
 private MockMvc mockMvc;
 private ObjectMapper jsonMapper;
 @Before
 public void setUp() {
 jsonMapper = new ObjectMapper();
 }
 @Test
 public void testGetPerson() throws Exception {
 Person personFongo = new Person();
 personFongo.setId(1);
 personFongo.setName("Name1");
 personFongo.setAddress("Address1");
 mongoTemplate.createCollection("person");
 mongoTemplate.insert(personFongo);
 ResultActions resultAction = mockMvc.perform(MockMvcRequestBuilders.get("http://localhost:8090/api/person/1"));
 resultAction.andExpect(MockMvcResultMatchers.status().is2xxSuccessful());
 MvcResult result = resultAction.andReturn();
 Person personResponse = jsonMapper.readValue(result.getResponse().getContentAsString(), Person.class);
 Assert.assertEquals(1, personResponse.getId());
 Assert.assertEquals("Name1", personResponse.getName());
 Assert.assertEquals("Address1", personResponse.getAddress());
 }
 @Test
 public void testCreatePerson() throws Exception {
 Person personRequest = new Person();
 personRequest.setId(1);
 personRequest.setName("Name1");
 personRequest.setAddress("Address1");
 String body = jsonMapper.writeValueAsString(personRequest);
 ResultActions resultAction = mockMvc.perform(MockMvcRequestBuilders.post("http://localhost:8090/api/person")
 .contentType(MediaType.APPLICATION_JSON).content(body));
 resultAction.andExpect(MockMvcResultMatchers.status().isCreated());
 Person personFongo = mongoTemplate.findOne(new Query(Criteria.where("id").is(1)), Person.class);
 Assert.assertEquals(personFongo.getName(), "Name1");
 Assert.assertEquals(personFongo.getAddress(), "Address1");
 }
}

Con esta clase de test estamos probando:

  1. Que al llamar a la operación GET /api/person/1 nos devuelve el documento que hemos insertado en la base de datos Fongo.
  2. Que al llamar a la operación POST /api/person, inserta correctamente en la base de datos Fongo el objeto pasado en el body.

Estos tests permiten probar todo el proceso completo que se realiza al llamar a un API REST, desde la propia llamada hasta el repositorio.

Con este post queremos demostrar lo sencillo que es incorporar tests integrados en nuestros servicios que usen una base de datos MongoDB, permitiendo simular todos los comportamientos que consideremos necesarios gracias a la versatilidad que ofrece Fongo, lo que desemboca en la construcción de servicios mucho más sólidos

Podéis encontrar el código de ejemplo en este repositorio de GitHub.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.