2015-09-09 1 views
10

Comment simuler un appel asynchrone d'une coroutine native vers une autre en utilisant unittest.mock.patch?Appel simulé d'un appel asynchrone en python 3.5

J'ai actuellement tout à fait une solution maladroite:

class CoroutineMock(MagicMock): 
    def __await__(self, *args, **kwargs): 
     future = Future() 
     future.set_result(self) 
     result = yield from future 
     return result 

Puis

class TestCoroutines(TestCase): 
    @patch('some.path', new_callable=CoroutineMock) 
    def test(self, mock): 
     some_action() 
     mock.assert_called_with(1,2,3) 

Cela fonctionne, mais semble laid. Y a-t-il une façon plus pythonique de faire ça?

+0

cette maquette ne fonctionne pas non plus avec asyncio.await en raison de asyncio.tasks.ensure_future – Zozz

Répondre

5

Sous-classe MagicMock propagera votre classe personnalisée pour tous les simulacres générés à partir de votre simulacre coroutine. Par exemple, AsyncMock().__str__ deviendra également un AsyncMock ce qui n'est probablement pas ce que vous cherchez. Au lieu de cela, vous pouvez définir une fabrique qui crée un Mock (ou un MagicMock) avec des arguments personnalisés, par exemple side_effect=coroutine(coro). En outre, il peut être judicieux de séparer la fonction coroutine de la coroutine (comme expliqué dans la section documentation).

Voici ce que je suis venu avec:

from asyncio import coroutine 

def CoroMock(): 
    coro = Mock(name="CoroutineResult") 
    corofunc = Mock(name="CoroutineFunction", side_effect=coroutine(coro)) 
    corofunc.coro = coro 
    return corofunc 

Une explication des différents objets:

  • corofunc: la fonction coroutine maquette
  • corofunc.side_effect(): le coroutine, généré pour chaque appel
  • corofunc.coro: le faux utilisé par la coroutine pour obtenir le résultat
  • corofunc.coro.return_value: la valeur retournée par la coroutine
  • corofunc.coro.side_effect: peut être utilisé pour soulever une exception

Exemple:

async def coro(a, b): 
    return await sleep(1, result=a+b) 

def some_action(a, b): 
    return get_event_loop().run_until_complete(coro(a, b)) 

@patch('__main__.coro', new_callable=CoroMock) 
def test(corofunc): 
    a, b, c = 1, 2, 3 
    corofunc.coro.return_value = c 
    result = some_action(a, b) 
    corofunc.assert_called_with(a, b) 
    assert result == c 
+0

ce ne fonctionne pas, side_effect = coroutine (coro), la coroutine n'est pas définie – Skorpeo

+0

@Skorpeo 'coroutine' doit être importé de' asyncio'. Édité. – Vincent

6

Une autre façon de se moquer de la coroutine est de faire de la coroutine, qui renvoie faux. De cette façon, vous pouvez simuler des coroutines qui seront passées en asyncio.wait ou asyncio.wait_for.

Cela rend plus coroutines universelle fait que la configuration des tests plus encombrants:

def make_coroutine(mock) 
    async def coroutine(*args, **kwargs): 
     return mock(*args, **kwargs) 
    return coroutine 


class Test(TestCase): 
    def setUp(self): 
     self.coroutine_mock = Mock() 
     self.patcher = patch('some.coroutine', 
          new=make_coroutine(self.coroutine_mock)) 
     self.patcher.start() 

    def tearDown(self): 
     self.patcher.stop() 
11

La solution est en fait assez simple: J'ai juste besoin de convertir la méthode __call__ de la maquette en coroutine:

class AsyncMock(MagicMock): 
    async def __call__(self, *args, **kwargs): 
     return super(AsyncMock, self).__call__(*args, **kwargs) 

Cela fonctionne parfaitement, quand un simulacre est appelé, le code reçoit la coroutine natif

+3

C'est agréable, mais il ne joue pas bien avec Autospec, qui est essentiellement obligatoire lors de l'utilisation MagicMock. Des idées sur la façon de le faire fonctionner? Je ne suis pas assez familier avec les internes ... – Symmetric

+1

Cela fonctionne parfaitement pour moi. Je l'ai utilisé comme ceci: '' '@ mock.patch ( 'my.path.asyncio.sleep', new_callable = AsyncMock, ) def test_stuff (sommeil): # code ' '' – karantan

2

Tout le monde est absent ce qui est probablement la plus simple et la plus claire solution:

@patch('some.path') 
def test(self, mock): 
    f = asyncio.Future() 
    f.set_result('whatever result you want') 
    process_smtp_message.return_value = f 
    mock.assert_called_with(1, 2, 3) 

rappelez-vous qu'une coroutine peut être pensée de juste comme une fonction qui est garantie pour retourner un avenir qui peut, à son tour être attendu.

+0

MERCI! C'est une façon simple et élégante de le faire! –

+0

qu'est-ce que c'est process_smtp_message.return_value = f? Aussi, où est l'appel à la fonction testée? – Skorpeo