michaelkryukov / mongomock_motor

Library for mocking AsyncIOMotorClient built on top of mongomock.
MIT License
102 stars 24 forks source link

AttributeError: 'UpdateResult' object has no attribute '_UpdateResult__acknowledged' #14

Closed Itay4 closed 2 years ago

Itay4 commented 2 years ago

i believe there is a regression in version 0.0.9 which causes our mocks in the tests to fail on the following:

Traceback (most recent call last):
File "/Users/itaykeren/.vscode/extensions/ms-python.python-2022.6.2/pythonFiles/lib/python/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_resolver.py", line 192, in _get_py_dictionary
attr = getattr(var, name)
AttributeError: \'UpdateResult\' object has no attribute \'_UpdateResult__acknowledged\

in our tests, we are mocking update_one to return UpdateResult({}, False) in order to mock an upsert failure - meaning result.acknowledged is set to False

this worked fine with previous versions, but breaks on 0.0.9

i suspect its related to the changes introduced in https://github.com/michaelkryukov/mongomock_motor/pull/13

can you please look into it?

michaelkryukov commented 2 years ago

Cab you provide complete example to investigate?

Itay4 commented 2 years ago

here you go:

source (named s.py):

from motor.motor_asyncio import AsyncIOMotorDatabase

async def upsert(db: AsyncIOMotorDatabase):
    result = await db["test-collection"].update_one(
        filter={"_id": "id"},
        update={
            "$set": {"field": "value"},
        },
        upsert=True,
    )
    if result.acknowledged is False:
        raise Exception()

test:

from pymongo.results import UpdateResult
import pytest
from mongomock_motor import AsyncMongoMockClient
from s import upsert

@pytest.mark.asyncio
async def test_upsert(mocker):
    db = AsyncMongoMockClient()["test-db"]
    mocker.patch.object(db["test-collection"], "update_one", return_value=UpdateResult({}, False))
    with pytest.raises(Exception):
        await upsert(db)

this test passes on v0.0.8 and fails on v0.0.9 with the following error:

    @pytest.mark.asyncio
    async def test_example(mocker):
        db = AsyncMongoMockClient()["test-db"]
        mocker.patch.object(db["test-collection"], "update_one", return_value=UpdateResult({}, False))
>       with pytest.raises(Exception):
E       Failed: DID NOT RAISE <class 'Exception'>

because the condition is not met (for acknowledged) and an exception is not raised

michaelkryukov commented 2 years ago

I looked into issue. Yeah, it was caused by changes in #13, but it's not that simple. You mocked property of the object that was returned from db["test-collection"] but use other object in the function. It's different objects.

>>> import pymongo
>>> client = pymongo.MongoClient(addr)
>>> database = client['database']
>>> database['collection'] == database['collection']
True
>>> database['collection'] is database['collection']
False
>>>

If you can pass collection to upsert – you can just patch.object that collection and pass it as argument. If you really need to use database object, it will require some changes to test code as well as to mongomock_motor.

michaelkryukov commented 2 years ago

Sample of "clean" solution:

from unittest.mock import patch, AsyncMock
from bson import ObjectId
from pymongo.results import UpdateResult
import pytest
from mongomock_motor import AsyncMongoMockClient

async def sample_function(collection):
    result = await collection.update_one(
        filter={"_id": ObjectId()},
        update={"$set": {"field": "value"}},
        upsert=True,
    )

    if result.acknowledged is False:
        raise RuntimeError()

@pytest.mark.asyncio
async def test_upsert():
    collection = AsyncMongoMockClient()['test']['test']

    with patch.object(collection, 'update_one', AsyncMock(return_value=UpdateResult({}, False))):
        with pytest.raises(RuntimeError):
            await sample_function(collection)
michaelkryukov commented 2 years ago

I've implemented possible improvement to issue in MR #15, you can check it out

Itay4 commented 2 years ago

looks good, thanks for looking into it !