Модульное тестирование с MongoDB

Моя база данных - MongoDB. Я пишу API уровня данных, чтобы абстрагировать детали реализации от клиентских приложений, то есть, по сути, я предоставляю единственный открытый интерфейс (объект, который действует как IDL).

Я проверяю свою логику, когда я иду в манере TDD. Перед каждым модульным тестом @Before вызывается метод для создания одноэлементной базы данных, после чего, когда тест завершается, @After Метод вызывается для удаления базы данных. Это помогает продвигать независимость среди юнит-тестов.

Почти все модульные тесты, то есть выполнение контекстного запроса, требуют, чтобы какая-то логика вставки выполнялась заранее. В моем общедоступном интерфейсе есть метод вставки - однако, кажется неправильным использовать этот метод в качестве логики предшественника для каждого модульного теста.

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

Что другие делают в этих ситуациях? То есть, как люди тестируют код, взаимодействующий с базой данных?

Кроме того, мой открытый интерфейс соединяется с базой данных, определенной во внешнем файле конфигурации - кажется неправильным использовать это соединение для моего модульного тестирования - опять же, ситуация, которая выиграет от какого-то насмешки?

5 ответов

Решение

Как пишет sbridges в этом посте, плохой идеей является отсутствие выделенного сервиса (иногда также известного как репозиторий или DAO), который абстрагирует доступ к данным от логики. Тогда вы можете проверить логику, предоставив макет DAO.

Другой подход, который я использую, - создать объект Mock of the Mongo (например, PowerMockito) и затем вернуть соответствующие результаты. Это потому, что вам не нужно проверять, работает ли база данных в модульных тестах, но более того, вы должны проверить, был ли правильный запрос отправлен в базу данных.

Mongo mongo = PowerMockito.mock(Mongo.class);
DB db = PowerMockito.mock(DB.class);
DBCollection dbCollection = PowerMockito.mock(DBCollection.class);

PowerMockito.when(mongo.getDB("foo")).thenReturn(db);
PowerMockito.when(db.getCollection("bar")).thenReturn(dbCollection);

MyService svc = new MyService(mongo); // Use some kind of dependency injection
svc.getObjectById(1);

PowerMockito.verify(dbCollection).findOne(new BasicDBObject("_id", 1));

Это также будет вариант. Конечно создание макетов и возврат соответствующих объектов только что закодировано, как пример выше.

Технически тесты, которые обращаются к базе данных (nosql или иным образом), не являются модульными тестами, поскольку тесты - это тестирование взаимодействия с внешней системой, а не просто тестирование изолированной единицы кода. Однако тесты, которые обращаются к базе данных, часто бывают чрезвычайно полезными и достаточно быстрыми для запуска с другими модульными тестами.

Обычно у меня есть интерфейс службы (например, UserService), который включает в себя всю логику для работы с базой данных. Код, основанный на UserService, может использовать поддельную версию UserService и легко тестируется.

При тестировании реализации Сервиса, который общается с Mongo (например, MongoUserService), проще всего написать некоторый Java-код, который запустит / остановит процесс Монго на локальном компьютере, и к которому ваш MongoUserService подключится, посмотрите этот вопрос для некоторых заметки

Вы можете попытаться смоделировать функциональность базы данных во время тестирования MongoUserService, но обычно это слишком подвержено ошибкам и не проверяет то, что вы действительно хотите проверить, то есть взаимодействие с реальной базой данных. Поэтому при написании тестов для MongoUserService вы устанавливаете состояние базы данных для каждого теста. Посмотрите на DbUnit для примера структуры для работы с базой данных.

Я написал поддельную реализацию MongoDB на Java: mongo-java-server

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

пример

MongoServer server = new MongoServer(new MemoryBackend());
// bind on a random local port
InetSocketAddress serverAddress = server.bind();

MongoClient client = new MongoClient(new ServerAddress(serverAddress));

DBCollection coll = client.getDB("testdb").getCollection("testcoll");
// creates the database and collection in memory and inserts the object
coll.insert(new BasicDBObject("key", "value"));

assertEquals(1, collection.count());
assertEquals("value", collection.findOne().get("key"));

client.close();
server.shutdownNow();

Сегодня я думаю, что лучше всего использовать библиотеку testcontainers (Java) или порт testcontainers-python на Python. Это позволяет использовать образы Docker с юнит-тестами. Для запуска контейнера в коде Java просто создайте экземпляр объекта GenericContainer ( пример):

GenericContainer mongo = new GenericContainer("mongo:latest")
    .withExposedPorts(27017);

MongoClient mongoClient = new MongoClient(mongo.getContainerIpAddress(), mongo.getMappedPort(27017));
MongoDatabase database = mongoClient.getDatabase("test");
MongoCollection<Document> collection = database.getCollection("testCollection");

Document doc = new Document("name", "foo")
        .append("value", 1);
collection.insertOne(doc);

Document doc2 = collection.find(new Document("name", "foo")).first();
assertEquals("A record can be inserted into and retrieved from MongoDB", 1, doc2.get("value"));

или на Python ( пример):

mongo = GenericContainer('mongo:latest')
mongo.with_bind_ports(27017, 27017)

with mongo_container:
    def connect():
        return MongoClient("mongodb://{}:{}".format(mongo.get_container_host_ip(),
                                                    mongo.get_exposed_port(27017)))

    db = wait_for(connect).primer
    result = db.restaurants.insert_one(
        # JSON as dict object
    )

    cursor = db.restaurants.find({"field": "value"})
    for document in cursor:
        print(document)

Я удивлен, что никто не советовал использовать fakemongo до сих пор. Он очень хорошо эмулирует клиент mongo, и все это работает на одной JVM с тестами - поэтому интеграционные тесты становятся надежными и технически намного ближе к истинным "модульным тестам", так как взаимодействие с внешними системами не происходит. Это похоже на использование встроенного H2 для модульного тестирования вашего кода SQL. Я был очень счастлив использовать fakemongo в модульных тестах, которые тестируют код интеграции базы данных в сквозной манере. Рассмотрим эту конфигурацию в контексте тестовой весны:

@Configuration
@Slf4j
public class FongoConfig extends AbstractMongoConfiguration {
    @Override
    public String getDatabaseName() {
        return "mongo-test";
    }

    @Override
    @Bean
    public Mongo mongo() throws Exception {
        log.info("Creating Fake Mongo instance");
        return new Fongo("mongo-test").getMongo();
    }

    @Bean
    @Override
    public MongoTemplate mongoTemplate() throws Exception {
        return new MongoTemplate(mongo(), getDatabaseName());
    }

}

При этом вы можете протестировать свой код, который использует MongoTemplate из контекста Spring, и в сочетании с nosql-unit, jsonunit и т. Д. Вы получите надежные модульные тесты, которые охватывают моно-код запроса.

@Test
@UsingDataSet(locations = {"/TSDR1326-data/TSDR1326-subject.json"}, loadStrategy = LoadStrategyEnum.CLEAN_INSERT)
@DatabaseSetup({"/TSDR1326-data/dbunit-TSDR1326.xml"})
public void shouldCleanUploadSubjectCollection() throws Exception {
    //given
    JobParameters jobParameters = new JobParametersBuilder()
            .addString("studyId", "TSDR1326")
            .addString("execId", UUID.randomUUID().toString())
            .toJobParameters();

    //when
    //next line runs a Spring Batch ETL process loading data from SQL DB(H2) into Mongo
    final JobExecution res = jobLauncherTestUtils.launchJob(jobParameters);

    //then
    assertThat(res.getExitStatus()).isEqualTo(ExitStatus.COMPLETED);
    final String resultJson = mongoTemplate.find(new Query().with(new Sort(Sort.Direction.ASC, "topLevel.subjectId.value")),
            DBObject.class, "subject").toString();

    assertThatJson(resultJson).isArray().ofLength(3);
    assertThatDateNode(resultJson, "[0].topLevel.timestamp.value").isEqualTo(res.getStartTime());

    assertThatNode(resultJson, "[0].topLevel.subjectECode.value").isStringEqualTo("E01");
    assertThatDateNode(resultJson, "[0].topLevel.subjectECode.timestamp").isEqualTo(res.getStartTime());

    ... etc
}

Я использовал fakemongo без проблем с драйвером mongo 3.4, и сообщество действительно близко к выпуску версии, которая поддерживает драйвер 3.6 ( https://github.com/fakemongo/fongo/issues/316).

Другие вопросы по тегам