ARM-software / devlib

Library for interaction with and instrumentation of remote devices.
Apache License 2.0
47 stars 78 forks source link

[Testing] How to test asyncf methods? #620

Closed GitCaptain closed 1 year ago

GitCaptain commented 1 year ago

Hello, I'm new to python testing, so maybe my question will be obvious, but I'm wonder how to mock methods which uses asyncf decorator? It is preferred to use only existing dependencies/standard library. I didn't found any examples in code/documentation. E.g. let's suppose I want to unittest AndroidTarget.ps method. I want to mock execute to avoid using real device, currently I trying to use code like this (Python version is 3.7. ):

from unittest import TestCase
from unittest.mock import patch, MagicMock
from devlib import AndroidTarget

class AsyncMock(MagicMock):
    # This class is used to avoid `MagicMock can't be used in await expressions` error
    async def __call__(self, *args, **kwargs):
        return super().__call__(*args, **kwargs)

class TestAndroidTargetExample(TestCase):

    def side_effect(self):
        return []

    def test(self):
        with patch.object(AndroidTarget, 'connect', return_value=None), \
             patch.object(AndroidTarget, 'execute', new_callable=AsyncMock, side_effect=self.side_effect): # what should be patched ??
            target = AndroidTarget()
            assert len(target.ps()) == 0

I've tried to patch 'execute.asyn', but python complains that AndroidTarget do not have such method.
So how to test such methods?

marcbonnici commented 1 year ago

Hi, Apologies for the delay in replying.

We have not typically attempted to mock up a target with devlib in the past as I expect you would need to be able to accommodate a relativity large variety of commands to allow a target connection to be initiated.

@douglas-raillard-arm Have you attempted anything like this in the past on your side?

douglas-raillard-arm commented 1 year ago

I haven't and I don't think it's possible to have a general mock Target object as this would require emulating sysfs and so on. Now if it comes to a limited mocking that would expect a specific sequence of commands (e.g. replaying a recorded session) there is no reason it would not be possible.

I'm not familiar with unittest.mock but from that snippet with a custom callable I don't see any reason this could not be done. All asyncf decorator beyond what a typical decorators does is add an asyn attribute to the function.

douglas-raillard-arm commented 1 year ago

Everything seems to be working fine:

from unittest import TestCase
from unittest.mock import patch, MagicMock

class FutureWrap:
    def __init__(self, f):
        self.f = f

    def __await__(self):
        return self.f()
        yield

class AsyncShimMock(MagicMock):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.asyn = lambda *args, **kwargs: FutureWrap(lambda: self(*args, **kwargs))

class TheTarget:
    def execute(self, cmd):
        # This is never executed since we use side_effect to patch the call
        raise NeverExecuted
        print(cmd)
        return 'some output'

class TestTarget(TestCase):
    def test(self):

        def side_effect(cmd):
            return 'hello world'

        with patch.object(TheTarget, 'execute', new_callable=AsyncShimMock, side_effect=side_effect):
            target = TheTarget()
            x = target.execute('echo hello world')
            assert x == 'hello world'

from unittest import IsolatedAsyncioTestCase
class TestAyncTarget(IsolatedAsyncioTestCase):

    async def test(self):

        def side_effect(cmd):
            return 'hello world'

        with patch.object(TheTarget, 'execute', new_callable=AsyncShimMock, side_effect=side_effect):
            target = TheTarget()
            x = await target.execute.asyn('echo hello world')
            assert x == 'hello world'
GitCaptain commented 1 year ago

@douglas-raillard-arm , @marcbonnici , thank you, this solution works for me.