Статьи

Интеграционное тестирование с MongoDB и Spring Data

Интеграционное тестирование — часто пропускаемая область в развитии предприятия. Это связано, прежде всего, со сложностями в настройке необходимой инфраструктуры для интеграционного теста. Для приложений, поддерживаемых базами данных, довольно сложно и занимает много времени настроить базы данных для интеграционных тестов, а также очистить их после завершения тестирования (например, файлы данных, схемы и т. Д.), Чтобы обеспечить повторяемость тестов. Хотя было много инструментов (например, DBUnit) и механизмов (например, откат после теста), чтобы помочь в этом, сложность и проблемы были всегда.

Но если вы работаете с MongoDB, есть классный и простой способ для выполнения ваших модульных тестов, с почти простотой написания модульного теста с помощью макетов. С помощью EmbedMongo мы можем легко настроить встроенный экземпляр MongoDB для тестирования со встроенной поддержкой очистки после завершения тестирования. В этой статье мы рассмотрим пример, в котором EmbedMongo используется с JUnit для интеграционного тестирования реализации репозитория.

Вот технологический стек, который мы будем использовать.

  • MongoDB 2.2.0
  • EmbedMongo 1.26
  • Spring Data — Mongo 1.0.3
  • Spring Framework 3.1

Maven POM для вышеуказанной настройки выглядит следующим образом.

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.yohanliyanage.blog.mongoit</groupId>
  <artifactId>mongo-it</artifactId>
  <version>1.0</version>
  <dependencies>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-mongodb</artifactId>
      <version>1.0.3.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.1.3.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>de.flapdoodle.embed</groupId>
      <artifactId>de.flapdoodle.embed.mongo</artifactId>
      <version>1.26</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Или, если вы предпочитаете Gradle (кстати, Gradle — это отличный инструмент для сборки, который вы должны проверить, если вы еще этого не сделали).

apply plugin: 'java'
apply plugin: 'eclipse'

sourceCompatibility = 1.6
group = "com.yohanliyanage.blog.mongoit"
version = '1.0'

ext.springVersion = '3.1.3.RELEASE'
ext.junitVersion = '4.10'
ext.springMongoVersion = '1.0.3.RELEASE'
ext.embedMongoVersion = '1.26'

repositories {
    mavenCentral()
    maven { url 'http://repo.springsource.org/release' }
}

dependencies {
    compile "org.springframework:spring-context:${springVersion}"
    compile "org.springframework.data:spring-data-mongodb:${springMongoVersion}"
    testCompile "junit:junit:${junitVersion}"
    testCompile "de.flapdoodle.embed:de.flapdoodle.embed.mongo:${embedMongoVersion}"
}

Для начала вот документ, который мы будем хранить в Монго.

package com.yohanliyanage.blog.mongoit.model;

import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;

/**
* A Sample Document.
*
* @author Yohan Liyanage
*
*/
@Document
public class Sample {

    @Indexed
    private String key;

    private String value;

    public Sample(String key, String value) {
        super();
        this.key = key;
        this.value = value;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

}

Чтобы помочь с хранением и управлением этим документом, давайте напишем простую реализацию репозитория. Интерфейс репозитория выглядит следующим образом.

package com.yohanliyanage.blog.mongoit.repository;

import java.util.List;

import com.yohanliyanage.blog.mongoit.model.Sample;

/**
* Sample Repository API.
*
* @author Yohan Liyanage
*
*/
public interface SampleRepository {

    /**
* Persists the given Sample.
* @param sample
*/
    void save(Sample sample);
    
    /**
* Returns the list of samples with given key.
* @param sample
* @return
*/
    List<Sample> findByKey(String key);
}

И реализация …

package com.yohanliyanage.blog.mongoit.repository;

import java.util.List;

import static org.springframework.data.mongodb.core.query.Query.query;
import static org.springframework.data.mongodb.core.query.Criteria.*;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.stereotype.Repository;

import com.yohanliyanage.blog.mongoit.model.Sample;

/**
* Sample Repository MongoDB Implementation.
*
* @author Yohan Liyanage
*
*/
@Repository
public class SampleRepositoryMongoImpl implements SampleRepository {

    @Autowired
    private MongoOperations mongoOps;
    
    /**
* {@inheritDoc}
*/
    public void save(Sample sample) {
        mongoOps.save(sample);
    }

    /**
* {@inheritDoc}
*/
    public List<Sample> findByKey(String key) {
        return mongoOps.find(query(where("key").is(key)), Sample.class);
    }

    /**
* Sets the MongoOps implementation.
*
* @param mongoOps the mongoOps to set
*/
    public void setMongoOps(MongoOperations mongoOps) {
        this.mongoOps = mongoOps;
    }

}

Для этого нам нужна конфигурация Spring Bean. Обратите внимание, что нам не нужно это для тестирования. Но для завершения я включил это. Конфигурация XML выглядит следующим образом.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:mongo="http://www.springframework.org/schema/data/mongo"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/data/mongo http://www.springframework.org/schema/data/mongo/spring-mongo-1.0.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

	<!-- Enable Annotation Driven Configuration -->
	<context:annotation-config />
	
	<!-- Component Scan Packages for Annotation Driven Configuration -->
	<context:component-scan base-package="com.yohanliyanage.blog.mongoit.repository" />

	<!-- Mongo DB -->
	<mongo:mongo host="127.0.0.1" port="27017" />
	
	<!-- Mongo DB Factory -->
	<mongo:db-factory dbname="mongoit" mongo-ref="mongo"/>
	
	<!-- Mongo Template -->
	<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
		<constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
	</bean>
	
</beans>

И теперь мы готовы написать интеграционный тест для нашей реализации репозитория с использованием Embed Mongo.

В идеале интеграционные тесты должны быть помещены в отдельный исходный каталог, так же, как мы размещаем наши модульные тесты (например, src / test / java => src /gration-test / java). Тем не менее, ни Maven, ни Gradle не поддерживают это «из коробки» (пока — v1.2. Для Gradle продолжается обсуждение этой возможности).

Тем не менее, Maven и Gradle являются гибкими, поэтому вы можете настроить POM / build.gradle для этого. Однако, чтобы сделать это обсуждение простым и сфокусированным, я буду помещать интеграционные тесты в ‘src / test / java’, но я не рекомендую это для реального приложения.

Давайте начнем писать тест интеграции. Сначала давайте начнем с простого теста на основе JUnit для методов.

package com.yohanliyanage.blog.mongoit.repository;

import static org.junit.Assert.fail;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * Integration Test for {@link SampleRepositoryMongoImpl}.
 * 
 * @author Yohan Liyanage
 */
public class SampleRepositoryMongoImplIntegrationTest {

    private SampleRepositoryMongoImpl repoImpl;

    @Before
    public void setUp() throws Exception {
        repoImpl = new SampleRepositoryMongoImpl();
    }

    @After
    public void tearDown() throws Exception {
    }
    

    @Test
    public void testSave() {
        fail("Not yet implemented");
    }

    @Test
    public void testFindByKey() {
        fail("Not yet implemented");
    }

}

Когда этот тестовый пример JUnit инициализируется, нам нужно запустить EmbedMongo, чтобы запустить встроенный сервер Mongo. Также, когда тестовый пример заканчивается, нам нужно очистить базу данных. Ниже приведен фрагмент кода.

package com.yohanliyanage.blog.mongoit.repository;

import static org.junit.Assert.fail;

import java.io.IOException;

import org.junit.*;
import org.springframework.data.mongodb.core.MongoTemplate;

import com.mongodb.Mongo;
import com.yohanliyanage.blog.mongoit.model.Sample;

import de.flapdoodle.embed.mongo.MongodExecutable;
import de.flapdoodle.embed.mongo.MongodProcess;
import de.flapdoodle.embed.mongo.MongodStarter;
import de.flapdoodle.embed.mongo.config.MongodConfig;
import de.flapdoodle.embed.mongo.config.RuntimeConfig;
import de.flapdoodle.embed.mongo.distribution.Version;
import de.flapdoodle.embed.process.extract.UserTempNaming;

/**
* Integration Test for {@link SampleRepositoryMongoImpl}.
*
* @author Yohan Liyanage
*/
public class SampleRepositoryMongoImplIntegrationTest {

    private static final String LOCALHOST = "127.0.0.1";
    private static final String DB_NAME = "itest";
    private static final int MONGO_TEST_PORT = 27028;
    
    private SampleRepositoryMongoImpl repoImpl;

    private static MongodProcess mongoProcess;
    private static Mongo mongo;
    
    private MongoTemplate template;
    

    @BeforeClass
    public static void initializeDB() throws IOException {

        RuntimeConfig config = new RuntimeConfig();
        config.setExecutableNaming(new UserTempNaming());

        MongodStarter starter = MongodStarter.getInstance(config);

        MongodExecutable mongoExecutable = starter.prepare(new MongodConfig(Version.V2_2_0, MONGO_TEST_PORT, false));
        mongoProcess = mongoExecutable.start();

        mongo = new Mongo(LOCALHOST, MONGO_TEST_PORT);
        mongo.getDB(DB_NAME);
    }

    @AfterClass
    public static void shutdownDB() throws InterruptedException {
        mongo.close();
        mongoProcess.stop();
    }

    
    @Before
    public void setUp() throws Exception {
        repoImpl = new SampleRepositoryMongoImpl();
        template = new MongoTemplate(mongo, DB_NAME);
        repoImpl.setMongoOps(template);
    }

    @After
    public void tearDown() throws Exception {
        template.dropCollection(Sample.class);
    }

    @Test
    public void testSave() {
        fail("Not yet implemented");
    }

    @Test
    public void testFindByKey() {
        fail("Not yet implemented");
    }

}

Метод initializeDB () аннотируется @BeforeClass, чтобы запустить его перед созданием тестовых примеров Этот метод запускает встроенный экземпляр MongoDB, который привязан к данному порту, и предоставляет объект Mongo, который настроен на использование данной базы данных. Внутренне EmbedMongo создает необходимые файлы данных во временных каталогах.

Когда этот метод выполняется в первый раз, EmbedMongo загрузит необходимую реализацию Mongo (обозначенную в приведенном выше коде Version.V2_2_0), если она еще не существует. Это хорошая возможность, особенно когда речь идет о серверах непрерывной интеграции. Вам не нужно вручную устанавливать Mongo на каждом из серверов CI. Это на одну внешнюю зависимость для тестов меньше.

В методе shutdownDB (), который аннотируется @AfterClass, мы останавливаем процесс EmbedMongo. Это запускает необходимые очистки в EmbedMongo для удаления временных файлов данных, восстанавливая состояние до того, где оно было до выполнения Test Case.

Теперь мы обновили метод setUp () для создания объекта Spring MongoTemplate, который поддерживается экземпляром Mongo, предоставляемым EmbedMongo, и для настройки нашего RepoImpl с этим шаблоном. Метод tearDown () обновлен для удаления коллекции «Sample», чтобы каждый из наших методов тестирования начинался с чистого состояния.

Теперь это просто вопрос написания реальных методов тестирования.

Давайте начнем с теста метода сохранения.

@Test
public void testSave() {
    Sample sample = new Sample("TEST", "2");
    repoImpl.save(sample);
    
    int samplesInCollection = template.findAll(Sample.class).size();
    
    assertEquals("Only 1 Sample should exist collection, but there are " 
            + samplesInCollection, 1, samplesInCollection);
}

Мы создаем объект Sample, передаем его в repoImpl.save () и утверждаем, что в коллекции Sample есть только один Sample. Простой, простой материал.

А вот и метод теста для метода findByKey.

@Test
public void testFindByKey() {
    
    // Setup Test Data
    List<Sample> samples = Arrays.asList(
            new Sample("TEST", "1"), new Sample("TEST", "25"),
            new Sample("TEST2", "66"), new Sample("TEST2", "99"));
    
    for (Sample sample : samples) {
        template.save(sample);
    }
    
    // Execute Test
    List<Sample> matches = repoImpl.findByKey("TEST");
    
    // Note: Since our test data (populateDummies) have only 2
    // records with key "TEST", this should be 2
    assertEquals("Expected only two samples with key TEST, but there are "
            + matches.size(), 2, matches.size());
}

Сначала мы настраиваем данные, добавляя набор объектов Sample в хранилище данных. Здесь важно, чтобы мы напрямую использовали template.save (), потому что repoImpl.save () является тестируемым методом. Мы не проверяем это здесь, поэтому мы используем базовый «доверенный» template.save () во время настройки данных. Это основная концепция в модульном / интеграционном тестировании. Затем мы выполняем тестируемый метод ‘findByKey’ и утверждаем, что только два примера соответствуют нашему запросу.

Аналогично, мы можем продолжать писать больше тестов для каждого из методов репозитория, включая отрицательные тесты. И вот последний файл теста интеграции.

package com.yohanliyanage.blog.mongoit.repository;

import static org.junit.Assert.*;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import org.junit.*;
import org.springframework.data.mongodb.core.MongoTemplate;

import com.mongodb.Mongo;
import com.yohanliyanage.blog.mongoit.model.Sample;

import de.flapdoodle.embed.mongo.MongodExecutable;
import de.flapdoodle.embed.mongo.MongodProcess;
import de.flapdoodle.embed.mongo.MongodStarter;
import de.flapdoodle.embed.mongo.config.MongodConfig;
import de.flapdoodle.embed.mongo.config.RuntimeConfig;
import de.flapdoodle.embed.mongo.distribution.Version;
import de.flapdoodle.embed.process.extract.UserTempNaming;

/**
 * Integration Test for {@link SampleRepositoryMongoImpl}.
 * 
 * @author Yohan Liyanage
 */
public class SampleRepositoryMongoImplIntegrationTest {

    private static final String LOCALHOST = "127.0.0.1";
    private static final String DB_NAME = "itest";
    private static final int MONGO_TEST_PORT = 27028;
    
    private SampleRepositoryMongoImpl repoImpl;

    private static MongodProcess mongoProcess;
    private static Mongo mongo;
    
    private MongoTemplate template;
    

    @BeforeClass
    public static void initializeDB() throws IOException {

        RuntimeConfig config = new RuntimeConfig();
        config.setExecutableNaming(new UserTempNaming());

        MongodStarter starter = MongodStarter.getInstance(config);

        MongodExecutable mongoExecutable = starter.prepare(new MongodConfig(Version.V2_2_0, MONGO_TEST_PORT, false));
        mongoProcess = mongoExecutable.start();

        mongo = new Mongo(LOCALHOST, MONGO_TEST_PORT);
        mongo.getDB(DB_NAME);
    }

    @AfterClass
    public static void shutdownDB() throws InterruptedException {
        mongo.close();
        mongoProcess.stop();
    }

    
    @Before
    public void setUp() throws Exception {
        repoImpl = new SampleRepositoryMongoImpl();
        template = new MongoTemplate(mongo, DB_NAME);
        repoImpl.setMongoOps(template);
    }

    @After
    public void tearDown() throws Exception {
        template.dropCollection(Sample.class);
    }
    

    @Test
    public void testSave() {
        Sample sample = new Sample("TEST", "2");
        repoImpl.save(sample);
        
        int samplesInCollection = template.findAll(Sample.class).size();
        
        assertEquals("Only 1 Sample should exist in collection, but there are " 
                + samplesInCollection, 1, samplesInCollection);
    }

    @Test
    public void testFindByKey() {
        
        // Setup Test Data
        List<Sample> samples = Arrays.asList(
                new Sample("TEST", "1"), new Sample("TEST", "25"), 
                new Sample("TEST2", "66"),  new Sample("TEST2", "99"));
        
        for (Sample sample : samples) {
            template.save(sample);
        }        
        
        // Execute Test
        List<Sample> matches = repoImpl.findByKey("TEST");
        
        // Note: Since our test data (populateDummies) have only 2 
        // records with key "TEST", this should be 2
        assertEquals("Expected only two samples with key TEST, but there are " 
                + matches.size(), 2, matches.size());
    }
    
}

Кроме того, одной из ключевых проблем, связанных с интеграционными тестами, является время выполнения. Мы все хотим сохранить как можно меньше времени выполнения тестов, в идеале пару секунд, чтобы убедиться, что мы можем запустить все тесты во время CI с минимальным временем сборки и проверки. Однако поскольку интеграционные тесты основаны на базовой инфраструктуре, обычно для проведения интеграционных тестов требуется время. Но с EmbedMongo это не так. На моей машине вышеупомянутый набор тестов выполняется за 1,8 секунды, и каждый метод тестирования занимает всего лишь. 166 секунд максимум. Смотрите скриншот ниже.

Интеграционные тесты MongoDB

Я загрузил код для вышеупомянутого проекта в GitHub. Вы можете скачать / клонировать его отсюда: https://github.com/yohanliyanage/blog-mongo-integration-tests . Для получения дополнительной информации о EmbedMongo, обратитесь к их сайту на GitHub https://github.com/flapdoodle-oss/embedmongo.flapdoodle.de .