Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock a tornado coroutine function using mock framework for unit testing?

The title simply described my problem. I would like to mock "_func_inner_1" with specific return value. Thanks for any advises :)

code under test:

from tornado.gen import coroutine, Return
from tornado.testing import gen_test
from tornado.testing import AsyncTestCase

import mock

@coroutine
def _func_inner_1():
    raise Return(1)

@coroutine
def _func_under_test_1():
    temp = yield _func_inner_1()
    raise Return(temp + 1)

But, this intuitive solution not work

class Test123(AsyncTestCase):

    @gen_test
    @mock.patch(__name__ + '._func_inner_1')
    def test_1(self, mock_func_inner_1):
        mock_func_inner_1.side_effect = Return(9)
        result_1 = yield _func_inner_1()
        print 'result_1', result_1
        result = yield _func_under_test_1()
        self.assertEqual(10, result, result)

With below error, seems _func_inner_1 is not patched due to it's coroutine nature

AssertionError: 2

if I add coroutine to patch returned mock function

@gen_test
@mock.patch(__name__ + '._func_inner_1')
def test_1(self, mock_func_inner_1):
    mock_func_inner_1.side_effect = Return(9)
    mock_func_inner_1 = coroutine(mock_func_inner_1)
    result_1 = yield _func_inner_1()
    print 'result_1', result_1
    result = yield _func_under_test_1()
    self.assertEqual(10, result, result)

the error becomes:

Traceback (most recent call last):
  File "tornado/testing.py", line 118, in __call__
    result = self.orig_method(*args, **kwargs)
  File "tornado/testing.py", line 494, in post_coroutine
    timeout=timeout)
  File "tornado/ioloop.py", line 418, in run_sync
    return future_cell[0].result()
  File "tornado/concurrent.py", line 109, in result
    raise_exc_info(self._exc_info)
  File "tornado/gen.py", line 175, in wrapper
    yielded = next(result)
  File "coroutine_unit_test.py", line 39, in test_1
    mock_func_inner_1 = coroutine(mock_func_inner_1)
  File "tornado/gen.py", line 140, in coroutine
    return _make_coroutine_wrapper(func, replace_callback=True)
  File "tornado/gen.py", line 150, in _make_coroutine_wrapper
    @functools.wraps(func)
  File "functools.py", line 33, in update_wrapper
    setattr(wrapper, attr, getattr(wrapped, attr))
  File "mock.py", line 660, in __getattr__
    raise AttributeError(name)
AttributeError: __name__

This is the closest solution I can find, but the mocking function will NOT be reset after test case execution, unlike what patch does

@gen_test
def test_4(self):
    global _func_inner_1
    mock_func_inner_1 = mock.create_autospec(_func_inner_1)
    mock_func_inner_1.side_effect = Return(100)
    mock_func_inner_1 = coroutine(mock_func_inner_1)
    _func_inner_1 = mock_func_inner_1
    result = yield _func_under_test_1()
    self.assertEqual(101, result, result) 
like image 820
Jim Horng Avatar asked Jan 08 '15 09:01

Jim Horng


1 Answers

There are two issues here:

First is the interaction between @mock.patch and @gen_test. gen_test works by converting a generator into a "normal" function; mock.patch only works on normal functions (as far as the decorator can tell, the generator returns as soon as it reaches the first yield, so mock.patch undoes all its work). To avoid this problem, you can either reorder the decorators (always put @mock.patch before @gen_test, or use the with form of mock.patch instead of the decorator form.

Second, coroutines should never raise an exception. Instead, they return a Future which will contain a result or an exception. The special Return exception is encapsulated by the coroutine system; you would never raise it from a Future. When you create your mocks, you must create the appropriate Future and set it as the return value instead of using side_effect to raise on exception.

The complete solution is:

from tornado.concurrent import Future
from tornado.gen import coroutine, Return
from tornado.testing import gen_test
from tornado.testing import AsyncTestCase

import mock

@coroutine
def _func_inner_1():
    raise Return(1)

@coroutine
def _func_under_test_1():
    temp = yield _func_inner_1()
    raise Return(temp + 1)

class Test123(AsyncTestCase):

    @mock.patch(__name__ + '._func_inner_1')
    @gen_test
    def test_1(self, mock_func_inner_1):
        future_1 = Future()
        future_1.set_result(9)
        mock_func_inner_1.return_value = future_1
        result_1 = yield _func_inner_1()
        print 'result_1', result_1
        result = yield _func_under_test_1()
        self.assertEqual(10, result, result)

import unittest
unittest.main()
like image 153
Ben Darnell Avatar answered Oct 12 '22 03:10

Ben Darnell