Модульный тест Python не выполнен из-за ошибки StopIteration от Mock
Я тестирую объект класса, используя макет, который является ccxt.binance. Этот смоделированный объект передается в качестве аргумента функции, которую я тестирую. Первоначально это работает, потому что я был в состоянии до определенной степени проверить. Однако, когда я изменил значение указанного объекта, функция не учитывает значение и возвращает ошибку StopIteration. я что-то упустил с настройкой макета? Вот пример фрагмента кода.
Вот код, который тестируется
def exchange_history_load(args, ccxt_exchange):
# skip exchange if has no true fetchOHLCV or if exchange has no timeframe specific data
if ((not ccxt_exchange.has['fetchOHLCV']) or (ccxt_exchange.has['fetchOHLCV'] == 'emulated') or
(not ccxt_exchange.timeframes.get(args.timeframe))):
logging.warning(f'Skipping {ccxt_exchange.name}.')
return
# check if exchange is in database. Write to database if not.
exchange_db_data = get_exchange(ccxt_exchange.id)
if not exchange_db_data:
insert_exchange_to_db(ccxt_exchange.id, ccxt_exchange.name)
logging.info(f'Inserted {ccxt_exchange.name} to database.')
exchange_db_data = get_exchange(ccxt_exchange.id)
exchange_db_id = exchange_db_data[0]
logging.info(f'Fetched {ccxt_exchange.name} data.')
# load exchange markets
markets = ccxt_exchange.load_markets()
logging.info(f'Loaded {ccxt_exchange.name} markets.')
return exchange_db_id, markets
Вот тестовая функция
@patch('src.cron.historical_price.hp_load_data.insert_exchange_to_db')
@patch('src.cron.historical_price.hp_load_data.get_exchange')
@patch('ccxt.binance')
def test_exchange_history_load(mock_binance, mock_get_exchange, mock_insert_exchange_to_db):
# create sample arguments for function
args = argparse.Namespace(timeframe='1d')
# mock up sample return values for necessary functions
mock_get_exchange.side_effect = [None, (1, 'binance', 'Binance'), (1, 'binance', 'Binance')]
mock_binance.return_value.has.return_value = {'fetchOHLCV': True}
mock_binance.return_value.load_markets.return_value = {
'BTC/USDT': {
'id': 'btcusdt',
'symbol': 'BTC/USDT',
'base': 'BTC',
'quote': 'USDT',
'active': True
}
}
mock_binance.return_value.id.return_value = 'binance'
mock_binance.return_value.name.return_value = 'Binance'
ccxt_exchange = ccxt.binance()
# case 1: exchange has fetchOHLCV, timeframe, but is not in database
# test assertions with case 1
exchange_db_id, markets = exchange_history_load(args, ccxt_exchange)
assert type(exchange_db_id) == int
assert type(markets) == dict
assert mock_get_exchange.call_count == 2
calls = [call(mock_binance.return_value.id), call(mock_binance.return_value.id)]
mock_get_exchange.assert_has_calls(calls)
mock_insert_exchange_to_db.assert_called_once()
mock_binance.return_value.load_markets.assert_called_once()
# case 2: exchange has fetchOHLCV, timeframe, and is in database
# reset mock calls for case 2
mock_insert_exchange_to_db.reset_mock()
mock_get_exchange.reset_mock()
mock_binance.return_value.load_markets.reset_mock()
# test assertions with case 2
exchange_db_id, markets = exchange_history_load(args, ccxt_exchange)
assert type(exchange_db_id) == int
assert type(markets) == dict
mock_get_exchange.assert_called_once()
assert not mock_insert_exchange_to_db.called
mock_binance.return_value.load_markets.assert_called_once()
calls = [call(mock_binance.return_value.id)]
mock_get_exchange.assert_has_calls(calls)
# case 3: exchange doesn't have true fetchOHLCV
# reset_mock calls for case 3
mock_get_exchange.reset_mock()
mock_insert_exchange_to_db.reset_mock()
mock_binance.reset_mock()
mock_binance.return_value.has.return_value = {'fetchOHLCV': True}
# mock_binance.return_value.load_markets.reset_mock()
# test assertions with case 3
exchange_db_id, markets = exchange_history_load(args, ccxt_exchange)
# assert not exchange_db_id
# assert not markets
assert not mock_get_exchange.called
assert not mock_insert_exchange_to_db.called
assert not mock_binance.return_value.load_markets.called
Случай 1 и случай 2 работает. Но в случае 3 выдается ошибка.
mock_binance = <MagicMock name='binance' id='140285596974272'>
mock_get_exchange = <MagicMock name='get_exchange' id='140285596584312'>
mock_insert_exchange_to_db = <MagicMock name='insert_exchange_to_db' id='140285596584480'>
@patch('src.cron.historical_price.hp_load_data.insert_exchange_to_db')
@patch('src.cron.historical_price.hp_load_data.get_exchange')
@patch('ccxt.binance')
def test_exchange_history_load(mock_binance, mock_get_exchange, mock_insert_exchange_to_db):
# create sample arguments for function
args = argparse.Namespace(timeframe='1d')
# mock up sample return values for necessary functions
mock_get_exchange.side_effect = [None, (1, 'binance', 'Binance'), (1, 'binance', 'Binance')]
mock_binance.return_value.has.return_value = {'fetchOHLCV': True}
mock_binance.return_value.load_markets.return_value = {
'BTC/USDT': {
'id': 'btcusdt',
'symbol': 'BTC/USDT',
'base': 'BTC',
'quote': 'USDT',
'active': True
}
}
mock_binance.return_value.id.return_value = 'binance'
mock_binance.return_value.name.return_value = 'Binance'
ccxt_exchange = ccxt.binance()
# case 1: exchange is not in database
# test assertions with case 1
exchange_db_id, markets = exchange_history_load(args, ccxt_exchange)
assert type(exchange_db_id) == int
assert type(markets) == dict
assert mock_get_exchange.call_count == 2
calls = [call(mock_binance.return_value.id), call(mock_binance.return_value.id)]
mock_get_exchange.assert_has_calls(calls)
mock_insert_exchange_to_db.assert_called_once()
mock_binance.return_value.load_markets.assert_called_once()
# case 2: exchange is in database
# reset mock calls for case 2
mock_insert_exchange_to_db.reset_mock()
mock_get_exchange.reset_mock()
mock_binance.return_value.load_markets.reset_mock()
# test assertions with case 2
exchange_db_id, markets = exchange_history_load(args, ccxt_exchange)
assert type(exchange_db_id) == int
assert type(markets) == dict
mock_get_exchange.assert_called_once()
assert not mock_insert_exchange_to_db.called
mock_binance.return_value.load_markets.assert_called_once()
calls = [call(mock_binance.return_value.id)]
mock_get_exchange.assert_has_calls(calls)
# case 3: exchange doesn't have true fetchOHLCV
# reset_mock calls for case 3
mock_get_exchange.reset_mock()
mock_insert_exchange_to_db.reset_mock()
mock_binance.reset_mock()
mock_binance.return_value.has.return_value = {'fetchOHLCV': True}
# mock_binance.return_value.load_markets.reset_mock()
# test assertions with case 3
> exchange_db_id, markets = exchange_history_load(args, ccxt_exchange)
test_cron/test_historical_price/test_hp_load_data.py:89:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../src/cron/historical_price/hp_load_data.py:75: in exchange_history_load
exchange_db_data = get_exchange(ccxt_exchange.id)
/usr/lib/python3.6/unittest/mock.py:939: in __call__
return _mock_self._mock_call(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_mock_self = <MagicMock name='get_exchange' id='140285596584312'>
args = (<MagicMock name='binance().id' id='140285596659328'>,), kwargs = {}
self = <MagicMock name='get_exchange' id='140285596584312'>, _new_name = ''
_new_parent = None
_call = call(<MagicMock name='binance().id' id='140285596659328'>), seen = set()
skip_next_dot = False, do_method_calls = False, name = 'get_exchange'
def _mock_call(_mock_self, *args, **kwargs):
self = _mock_self
self.called = True
self.call_count += 1
_new_name = self._mock_new_name
_new_parent = self._mock_new_parent
_call = _Call((args, kwargs), two=True)
self.call_args = _call
self.call_args_list.append(_call)
self.mock_calls.append(_Call(('', args, kwargs)))
seen = set()
skip_next_dot = _new_name == '()'
do_method_calls = self._mock_parent is not None
name = self._mock_name
while _new_parent is not None:
this_mock_call = _Call((_new_name, args, kwargs))
if _new_parent._mock_new_name:
dot = '.'
if skip_next_dot:
dot = ''
skip_next_dot = False
if _new_parent._mock_new_name == '()':
skip_next_dot = True
_new_name = _new_parent._mock_new_name + dot + _new_name
if do_method_calls:
if _new_name == name:
this_method_call = this_mock_call
else:
this_method_call = _Call((name, args, kwargs))
_new_parent.method_calls.append(this_method_call)
do_method_calls = _new_parent._mock_parent is not None
if do_method_calls:
name = _new_parent._mock_name + '.' + name
_new_parent.mock_calls.append(this_mock_call)
_new_parent = _new_parent._mock_new_parent
# use ids here so as not to call __hash__ on the mocks
_new_parent_id = id(_new_parent)
if _new_parent_id in seen:
break
seen.add(_new_parent_id)
ret_val = DEFAULT
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
if not _callable(effect):
> result = next(effect)
E StopIteration
/usr/lib/python3.6/unittest/mock.py:998: StopIteration
2 ответа
Для тех, кто ищет ответ. Эта проблема возникает, когда один из ваших макетов
side_effect
называется раз, когда
n
>
len(side_effect)
.
my_mock = MagicMock()
my_mock.side_effect = [1, 2]
...
n = 3
for i in range(n):
my_mock()
Это вызовет
StopIteration
ошибка.
Изменение значения, которое вы сделали для ccxt.binance, приводит к тому, что объект ccxt_exchange выходит за пределы допустимого диапазона. Таким образом, давая ошибку "StopIteration", для Ex -
In [1]: from fleeteng_fw_agent.hostinfo imfrom mock import MagicMock,
PropertyMock, call, patch
In [2]: mock_get_exchange = MagicMock()
In [3]: mock_get_exchange.side_effect = [None, (1, 'binance', 'Binance'), (1,
'binance', 'Binance')]
In [4]: mock_get_exchange
Out[4]: <MagicMock id='4310663120'>
In [5]: mock_get_exchange()
In [6]: mock_get_exchange()
Out[6]: (1, 'binance', 'Binance')
In [7]: mock_get_exchange()
Out[7]: (1, 'binance', 'Binance')
In [8]: mock_get_exchange()
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-8-4d63b6292e10> in <module>()
----> 1 mock_get_exchange()
/Users/**/ in __call__(_mock_self, *args, **kwargs)
954 # in the signature
955 _mock_self._mock_check_sig(*args, **kwargs)
--> 956 return _mock_self._mock_call(*args, **kwargs)
957
958
/Users/**/ in _mock_call(_mock_self, *args, **kwargs)
1012
1013 if not _callable(effect):
-> 1014 result = next(effect)
1015 if _is_exception(result):
1016 raise result
StopIteration:
Мне нужно будет посмотреть на изменение значения ccxt.binancegive, чтобы узнать точную причину ошибки. Но я думаю, что приведенный выше пример должен указать вам на основную причину.