Модульное тестирование с 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).