Статьи

Black-Pipe Testing libmongoc и подключенное приложение в C

Поезд FКак и любая сетевая клиентская библиотека, libmongoc не может быть полностью протестирован как черный ящик. Традиционные тесты черного ящика вводят некоторый ввод и проверяют вывод — это проверяет только одну сторону системы за раз. Но у libmongoc две стороны, работающие согласованно. Одна сторона — это его публичный API, его структуры и функции и так далее. Другой — это связь по сети с сервером MongoDB. Только рассматривая его как черную трубу, мы можем полностью проверить его две стороны.

происхождения

Я начал думать о тестировании «черной трубы» в начале этого года. Я читал набор тестов libmongoc, готовясь принять проект от Кристиана Хергерта и Джейсона Кэри, и наткнулся на mock_server_tструктуру Кристиана . Тестовый код на С обычно не делает живое чтение, но я проснулся, когда увидел это. Он действительно написал сервер проводного протокола MongoDB для тестирования клиентской библиотеки?

Если вы знаете работу Кристиана Хергерта, вы знаете ответ. Конечно он имел. Его фиктивный сервер прослушивал случайный порт TCP, анализировал сетевые сообщения клиента и отправлял ответы MongoDB. В то время mock_server_tиспользовались обратные вызовы: вы создали фиктивный сервер с указателем на функцию, которая обрабатывает запросы, и выбрали способ ответа. И если вы думаете, что обратные вызовы неуклюжи в Javascript или Python, попробуйте их в C.

Несмотря на неловкий API, макет сервера был незаменим для определенных тестов. Например, у Кристиана был фиктивный сервер, который сообщил, что он использует только протоколы 10 и 11 протокола проводной связи. Поскольку последняя версия протокола MongoDB — только 3, драйвер не знает, как общаться с таким футуристическим сервером, и должен отказаться, но Единственный способ проверить это поведение — симулировать сервер.

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

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

Поезд

Эволюция: от C до Python

Я еще не взял на себя руководство libmongoc — я заканчивал работу над Python. Итак, вдохновленный идеей Кристиана, я написал макет сервера на Python, который называется MockupDB . MockupDB — тема моей предыдущей статьи из этой серии: «Тестирование PyMongo как чёрной трубы».

Поскольку я работал на своем родном языке Python, я мог позволить себе быть привередливым в интерфейсе MockupDB. Я не хотел обратных вызовов, черт возьми, я хотел сделать что-то хорошее! Как я писал в статье MockupDB, я разработал программный интерфейс будущего, который позволяет мне аккуратно чередовать операции клиента и сервера в одной тестовой функции:

from mockupdb import MockupDB, Command, go
from pymongo import MongoClient
def test():
   server = MockupDB(auto_ismaster={"maxWireVersion": 3})
   server.run()

   client = MongoClient(server.uri)
   collection = client.db.collection

   future = go(collection.insert_one, {"_id": 1})
   request = server.receives(Command({"insert": "collection"}))
   request.reply({'ok': 1})
   assert(future().inserted_id == 1)

Давайте разберемся с этим. Я использую goфункцию MockupDB для запуска операции PyMongo в фоновом потоке, получая дескриптор его будущего результата:

future = go(collection.insert_one, {"_id": 1})

Драйвер отправляет команду «вставка» на фиктивный сервер и блокирует ожидание ответа сервера. Я получаю эту команду с сервера и проверяю, что она имеет ожидаемый формат:

request = server.receives(Command({"insert": "collection"}))

MockupDB утверждает, что команда прибывает быстро и имеет правильный формат, прежде чем она вернет мне команду. Я отвечаю клиенту, который разблокирует его и позволяет получить будущее значение:

request.reply({'ok': 1})
assert(future().inserted_id == 1)

Больше эволюции: от Python назад к C

Когда мы с Берни Хакеттом выпустили PyMongo 3.0 , я полностью посвятил себя работе над libmongoc. Я приступил к работе, обновляя его mock_server_tс идеями, которые я разработал в Python. Я написал пример с API, который я хотел:

mock_server_t *server;
mongoc_client_t *client;
mongoc_collection_t *collection;
bson_t *document;
bson_error_t error;
future_t *future;
request_t *request;

/* protocol version 3 includes the new "insert" command */
server = mock_server_with_autoismaster (3);
mock_server_run (server);

client = mongoc_client_new_from_uri (mock_server_get_uri (server));
collection = mongoc_client_get_collection (client, "test", "collection");
document = BCON_NEW ("_id", BCON_INT64 (1));
future = future_collection_insert (collection,
                                   MONGOC_INSERT_NONE,/* flags */
                                   document,
                                   NULL,              /* writeConcern */
                                   &error);

request = mock_server_receives_command (server, "test", MONGOC_QUERY_NONE,
                                        "{'insert': 'collection'}");

mock_server_replies_simple (request, "{'ok': 1}");
assert (future_get_bool (future));

future_destroy (future);
request_destroy (request);
bson_destroy (document);
mongoc_collection_destroy(collection);
mongoc_client_destroy(client);
mock_server_destroy (server);

Увы, С — это пролификс; это было настолько скудно, насколько я мог это сделать. Я сомневаюсь, что вы читаете этот блок кода. Давайте сосредоточимся на некоторых ключевых линиях.

Сначала запускается фиктивный сервер, который связывает неиспользуемый порт. Как и в Python, я подключаю настоящий клиентский объект к URI фиктивного сервера:

client = mongoc_client_new_from_uri (mock_server_get_uri (server));

Теперь я вставляю документ. Клиент отправляет команду «вставка» на фиктивный сервер и блокирует ожидание ответа:

future = future_collection_insert (collection,
                                   MONGOC_INSERT_NONE,/* flags */
                                   document,
                                   NULL,              /* writeConcern */
                                   &error);

future_collection_insertФункция запускает фоновый поток и запускает функцию libmongoc mongoc_collection_insert. Он возвращает будущее значение, которое будет разрешено после завершения фонового потока.

Тем временем фиктивный сервер получает клиентскую команду «вставки»:

request = mock_server_receives_command (server,
                                        "test",            /* DB name */
                                        MONGOC_QUERY_NONE, /* no flags */
                                        "{'insert': 'collection'}");

Это утверждение выполняет несколько целей. Во-первых, он ожидает (используя условную переменную) фоновый поток, чтобы отправить команду «вставить». Во-вторых, он проверяет, что команда имеет правильный формат: ее имя базы данных «test», ее флаги не установлены, сама команда называется «insert», а целевая коллекция называется «collection».

Тест завершается, когда я отвечаю клиенту:

mock_server_replies_simple (request, "{'ok': 1}");
assert (future_get_bool (future));

Это разблокирует фоновый поток. Будущее решается с возвращаемым значением mongoc_collection_insert. Я утверждаю, что его возвращаемое значение было true, то есть это удалось. Мой тестовый фреймворк обнаруживает, future_get_boolзаблокирован ли он: это означает, mongoc_collection_insertчто по какой-то причине он не заканчивается, и это также приведет к сбою моего теста.

Вывод

Когда я впервые увидел, что Кристиан Хергерт mock_server_tвдохновил меня , его блеск: для тестирования клиента MongoDB выдать себя за сервер MongoDB!

Я написал пакет MockupDB на Python, а затем пересмотрел фиктивный сервер Кристиана на C. По мере того, как я разрабатывал и использовал эту идею в течение прошлого года, я обобщил ее за пределы проблемы тестирования драйверов MongoDB. То, что я называю «тестом черного канала», относится к любому сетевому приложению, чье поведение API и сетевой протокол должны проверяться одновременно.