Торнадо, Мотор с mongomock для тестирования

Я пишу тестовый модуль для веб-приложения на основе торнадо. Приложение использует мотор в качестве разъема mongodb, и я хочу, чтобы мои тесты выполнялись на временной базе данных. Я использую технику насмешки над делегат -классом клиента коннектора следующим образом:

import json
import mock
import motor
import tornado.ioloop
import tornado.testing
import mongomock
import myapp


patch_motor_client = mock.patch('motor.motor_tornado.MotorClient.__delegate_class__', new=mongomock.MongoClient)
patch_motor_database = mock.patch('motor.motor_tornado.MotorDatabase.__delegate_class__', new=mock.MagicMock)

patch_motor_client.start()
patch_motor_database.start()


class TestHandlerBase(tornado.testing.AsyncHTTPTestCase):
    """
    Base test handler
    """

    def setUp(self):
        # Create your Application for testing
        self.application = myapp.app.Application()
        super(TestHandlerBase, self).setUp()

    def get_app(self):
        return self.application

    def get_new_ioloop(self):
        return tornado.ioloop.IOLoop.instance()


class TestMyHandler(TestHandlerBase):
    def test_post_ok(self):
        """
        POST a resource is OK
        """
        post_args = {
            'data': 'some data here..'
        }

        response = self.fetch('myapi/v1/scripts', method='POST', body=json.dumps(post_args))

        # assert status is 201
        self.assertEqual(response.code, 201)

Когда я запускаю свои тесты, я получаю эту ошибку:

  File "/data/.virtualenvs/myapp/lib/python3.5/site-packages/motor/core.py", line 162, in __getitem__
    return db_class(self, name)
  File "/data/.virtualenvs/myapp/lib/python3.5/site-packages/motor/core.py", line 217, in __init__
    client.delegate, name, **kwargs)
  File "/data/.virtualenvs/myapp/lib/python3.5/site-packages/pymongo/database.py", line 102, in __init__
    read_concern or client.read_concern)
  File "/data/.virtualenvs/myapp/lib/python3.5/site-packages/pymongo/common.py", line 614, in __init__
    raise TypeError("codec_options must be an instance of "
TypeError: codec_options must be an instance of bson.codec_options.CodecOptions

На данный момент я не могу заставить его работать, и мне интересно, возможно ли то, что я хочу сделать, с текущими версиями motor (1.2.1), mongomock (3.8.0) и tornado (4.5.3) или я что то пропустил?

Спасибо за все ваши предложения.

1 ответ

Я мог только заставить это работать с тяжелым исправлением обезьян (я думаю, mock.patch-ing был бы похож, но я не был заинтересован в том, чтобы отменить изменения).

Я определил следующие проблемы:

  1. Мотор, кажется, игнорирует __delegate_class__ время от времени, и конкретизировать фактическое pymongo Database или же Collection вместо delegate = _delegate or Collection(database.delegate, name))
  2. Motor оборачивает класс делегата и хочет, чтобы каждый ожидаемый атрибут существовал, чтобы он мог "асинхронизировать" их.
  3. Моторные "агностические" курсоры опираются на полуприватные детали курсоров Pymongo, например _refresh или же __dataпотому что им нужно вмешиваться в их манипуляции (снова асинхронизировать их, так как они IO). Курсоры Mongomock намного проще и не имеют таких атрибутов

И работал вокруг них так:

  1. Возможно, это исправимо в двигателе, но это доставило мне неприятности, поэтому мне пришлось начинать "обмотку мотора" только на уровне сбора (это не работало на клиенте или в базе данных) и взламывать, чтобы база данных mongomock создавала мотор Завернутые коллекции:
db = mongomock.Database(mongomock.MongoClient(), 'db_name')

# Monkeypatch get_collection so that collections are motor-wrapped
def create_motor_wrapped_mock_collection(
        name, codec_options=None, read_preference=None,
        write_concern=None, read_concern=None):
    if read_concern:
        raise NotImplementedError('Mongomock does not handle read_concern yet')
    collection = db._collections.get(name)
    if collection is None:
        delegate = mongomock.Collection(db, name, write_concern=write_concern)

        # wont be used, as we patch get_io_loop, but the MotorCollection ctor checks type
        fake_client = motor.motor_tornado.MotorClient()
        fake_db = motor.motor_tornado.MotorDatabase(fake_client, 'db_name')

        motor_collection = motor.motor_tornado.MotorCollection(fake_db, name, _delegate=delegate)
        collection = db._collections[name] = motor_collection
        collection.get_io_loop = lambda: tornado.ioloop.IOLoop.current()
    return collection

db.get_collection = create_motor_wrapped_mock_collection 

# Then use db in your code or patch it in
  1. Это проблема, которую вы бьете. Этого можно избежать путем сканирования необходимых атрибутов и определения поддельных, если они отсутствуют:
def _prepare_for_motor_wrapping(cls, wrapper_cls):

    # Motor expects all attributes to exist on a delegate, to generate wrapped methods/attributes, even the ones we
    # won't need. This patches in dummy attributes/methods so that Motor wrapping can succeed

    def gen_fake_method(name, on):
        def fake_method(*args, **kwargs):
            raise NotImplementedError(name + ' on ' + on)
        return fake_method

    attrs = list(wrapper_cls.__dict__.items()) + list(motor.core.AgnosticBaseProperties.__dict__.items())
    for k, v in attrs:
        attr_name = getattr(v, 'attr_name', None) or k
        if not hasattr(cls, attr_name) and isinstance(v, motor.metaprogramming.MotorAttributeFactory):
            if isinstance(v, motor.metaprogramming.ReadOnlyProperty):
                setattr(cls, attr_name, None)
            elif isinstance(v, motor.metaprogramming.Async) or isinstance(v, motor.metaprogramming.Unwrap):
                setattr(cls, attr_name, gen_fake_method(attr_name, cls.__name__))
            else:
                raise RuntimeError('Dont know how to fake %s' % v)

# We must clear the cache, as classes might have been generated already during some previous import
motor.metaprogramming._class_cache = {}

_prepare_for_motor_wrapping(mongomock.Database, motor.core.AgnosticDatabase)
motor.motor_tornado.MotorDatabase = motor.motor_tornado.create_motor_class(motor.core.AgnosticDatabase)

_prepare_for_motor_wrapping(mongomock.Collection, motor.core.AgnosticCollection) 
motor.motor_tornado.MotorCollection = motor.motor_tornado.create_motor_class(motor.core.AgnosticCollection)

По какой-то причине MotorClient должен быть оставлен без изменений.

  1. Я выбрался из патча to_list(), так как это единственное, что я использую в результатах coll.aggregate() и coll.find()
def _patch_aggregate_cursor():

    def curs_to_docs(docs_future, curs_future):
        curs = curs_future.result()
        docs_future.set_result(list(curs))

    def to_list(self, *args):
        mock_cursor_future = self.collection._async_aggregate(self.pipeline)
        docs_future = self._framework.get_future(self.get_io_loop())
        self._framework.add_future(
            self.get_io_loop(),
            mock_cursor_future,
            curs_to_docs, docs_future)
        return docs_future

    motor.core.AgnosticAggregationCursor.to_list = to_list


def _patch_generic_cursor():

    def to_list(self, *args):
        docs = list(self.delegate)
        docs_future = self._framework.get_future(self.get_io_loop())
        docs_future.set_result(docs)
        return docs_future

    motor.core.AgnosticCursor.to_list = to_list

Все это, вероятно, неполно и хрупко, поэтому я позволю вам судить, стоит ли оно усилий.

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