diff --git a/freqtrade/fiat_convert.py b/freqtrade/fiat_convert.py index 91e04fff9..6f9d3d3d5 100644 --- a/freqtrade/fiat_convert.py +++ b/freqtrade/fiat_convert.py @@ -1,12 +1,19 @@ +""" +Module that define classes to convert Crypto-currency to FIAT +e.g BTC to USD +""" + import logging import time - from pymarketcap import Pymarketcap logger = logging.getLogger(__name__) class CryptoFiat(): + """ + Object to describe what is the price of Crypto-currency in a FIAT + """ # Constants CACHE_DURATION = 6 * 60 * 60 # 6 hours @@ -49,6 +56,11 @@ class CryptoFiat(): class CryptoToFiatConverter(object): + """ + Main class to initiate Crypto to FIAT. + This object contains a list of pair Crypto, FIAT + This object is also a Singleton + """ __instance = None _coinmarketcap = None diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py index c89b20527..abccf065b 100644 --- a/freqtrade/strategy/default_strategy.py +++ b/freqtrade/strategy/default_strategy.py @@ -1,7 +1,9 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + import talib.abstract as ta +from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.strategy.interface import IStrategy -from pandas import DataFrame class_name = 'DefaultStrategy' diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index ce5f08cd2..9281e72ca 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -1,8 +1,22 @@ +""" +IStrategy interface +This module defines the interface to apply for strategies +""" + from abc import ABC, abstractmethod from pandas import DataFrame class IStrategy(ABC): + """ + Interface for freqtrade strategies + Defines the mandatory structure must follow any custom strategies + + Attributes you can use: + minimal_roi -> Dict: Minimal ROI designed for the strategy + stoploss -> float: optimal stoploss designed for the strategy + ticker_interval -> int: value of the ticker interval to use for the strategy + """ @property def name(self) -> str: """ @@ -11,13 +25,6 @@ class IStrategy(ABC): """ return self.__class__.__name__ - """ - Attributes you can use: - minimal_roi -> Dict: Minimal ROI designed for the strategy - stoploss -> float: optimal stoploss designed for the strategy - ticker_interval -> int: value of the ticker interval to use for the strategy - """ - @abstractmethod def populate_indicators(self, dataframe: DataFrame) -> DataFrame: """ diff --git a/freqtrade/strategy/strategy.py b/freqtrade/strategy/strategy.py index 2545e378c..97e260ee8 100644 --- a/freqtrade/strategy/strategy.py +++ b/freqtrade/strategy/strategy.py @@ -1,10 +1,12 @@ +""" +This module load custom strategies +""" import os import sys import logging import importlib from pandas import DataFrame -from typing import Dict from freqtrade.strategy.interface import IStrategy @@ -12,16 +14,36 @@ sys.path.insert(0, r'../../user_data/strategies') class Strategy(object): + """ + This class contains all the logic to load custom strategy class + """ __instance = None DEFAULT_STRATEGY = 'default_strategy' def __new__(cls): + """ + Used to create the Singleton + :return: Strategy object + """ if Strategy.__instance is None: Strategy.__instance = object.__new__(cls) return Strategy.__instance + def __init__(self): + if Strategy.__instance is None: + self.logger = None + self.minimal_roi = None + self.stoploss = None + self.ticker_interval = None + self.custom_strategy = None + def init(self, config): + """ + Load the custom class from config parameter + :param config: + :return: + """ self.logger = logging.getLogger(__name__) # Verify the strategy is in the configuration, otherwise fallback to the default strategy @@ -42,21 +64,22 @@ class Strategy(object): if 'stoploss' in config: self.custom_strategy.stoploss = config['stoploss'] self.logger.info( - "Override strategy \'stoploss\' with value in config file: {}.".format( - config['stoploss'] - ) + "Override strategy \'stoploss\' with value in config file: %s.", config['stoploss'] ) if 'ticker_interval' in config: self.custom_strategy.ticker_interval = config['ticker_interval'] self.logger.info( - "Override strategy \'ticker_interval\' with value in config file: {}.".format( - config['ticker_interval'] - ) + "Override strategy \'ticker_interval\' with value in config file: %s.", + config['ticker_interval'] ) + # Minimal ROI designed for the strategy self.minimal_roi = self.custom_strategy.minimal_roi + + # Optimal stoploss designed for the strategy self.stoploss = self.custom_strategy.stoploss + self.ticker_interval = self.custom_strategy.ticker_interval def _load_strategy(self, strategy_name: str) -> None: @@ -90,7 +113,7 @@ class Strategy(object): module = importlib.import_module(filename, __package__) custom_strategy = getattr(module, module.class_name) - self.logger.info("Load strategy class: {} ({}.py)".format(module.class_name, filename)) + self.logger.info("Load strategy class: %s (%s.py)", module.class_name, filename) return custom_strategy() @staticmethod @@ -126,20 +149,6 @@ class Strategy(object): return path - def minimal_roi(self) -> Dict: - """ - Minimal ROI designed for the strategy - :return: Dict: Value for the Minimal ROI - """ - return - - def stoploss(self) -> float: - """ - Optimal stoploss designed for the strategy - :return: float | return None to disable it - """ - return self.custom_strategy.stoploss - def populate_indicators(self, dataframe: DataFrame) -> DataFrame: """ Populate indicators that will be used in the Buy and Sell strategy diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 1e53648e1..053b980f7 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -3,10 +3,13 @@ from datetime import datetime from unittest.mock import MagicMock from functools import reduce +import json import arrow import pytest from jsonschema import validate from telegram import Chat, Message, Update +from freqtrade.analyze import parse_ticker_dataframe +from freqtrade.strategy.strategy import Strategy from freqtrade.misc import CONF_SCHEMA @@ -256,3 +259,16 @@ def ticker_history_without_bv(): "T": "2017-11-26T09:00:00" } ] + + +@pytest.fixture +def result(): + with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: + return parse_ticker_dataframe(json.load(data_file)) + + +@pytest.fixture +def default_strategy(): + strategy = Strategy() + strategy.init({'strategy': 'default_strategy'}) + return strategy diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index a0386733d..9761fff3a 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1,8 +1,9 @@ -# pragma pylint: disable=missing-docstring,C0103 +# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement +# pragma pylint: disable=protected-access from unittest.mock import MagicMock -from requests.exceptions import RequestException from random import randint import logging +from requests.exceptions import RequestException import pytest from freqtrade import OperationalException @@ -30,7 +31,7 @@ def test_init(default_conf, mocker, caplog): ) in caplog.record_tuples -def test_init_exception(default_conf, mocker): +def test_init_exception(default_conf): default_conf['exchange']['name'] = 'wrong_exchange_name' with pytest.raises( @@ -171,7 +172,7 @@ def test_get_balances_prod(default_conf, mocker): # This test is somewhat redundant with # test_exchange_bittrex.py::test_exchange_bittrex_get_ticker -def test_get_ticker(default_conf, mocker, ticker): +def test_get_ticker(default_conf, mocker): maybe_init_api(default_conf, mocker) api_mock = MagicMock() tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}} @@ -200,7 +201,7 @@ def test_get_ticker(default_conf, mocker, ticker): assert ticker['ask'] == 1 -def test_get_ticker_history(default_conf, mocker, ticker): +def test_get_ticker_history(default_conf, mocker): api_mock = MagicMock() tick = 123 api_mock.get_ticker_history = MagicMock(return_value=tick) @@ -251,7 +252,7 @@ def test_get_order(default_conf, mocker): api_mock = MagicMock() api_mock.get_order = MagicMock(return_value=456) mocker.patch('freqtrade.exchange._API', api_mock) - assert 456 == exchange.get_order('X') + assert exchange.get_order('X') == 456 def test_get_name(default_conf, mocker): @@ -271,16 +272,16 @@ def test_get_fee(default_conf, mocker): assert get_fee() == 0.0025 -def test_exchange_misc(default_conf, mocker): +def test_exchange_misc(mocker): api_mock = MagicMock() mocker.patch('freqtrade.exchange._API', api_mock) exchange.get_markets() - assert 1 == api_mock.get_markets.call_count + assert api_mock.get_markets.call_count == 1 exchange.get_market_summaries() - assert 1 == api_mock.get_market_summaries.call_count + assert api_mock.get_market_summaries.call_count == 1 api_mock.name = 123 - assert 123 == exchange.get_name() + assert exchange.get_name() == 123 api_mock.fee = 456 - assert 456 == exchange.get_fee() + assert exchange.get_fee() == 456 exchange.get_wallet_health() - assert 1 == api_mock.get_wallet_health.call_count + assert api_mock.get_wallet_health.call_count == 1 diff --git a/freqtrade/tests/exchange/test_exchange_bittrex.py b/freqtrade/tests/exchange/test_exchange_bittrex.py index 949acf25f..1d1c7fa24 100644 --- a/freqtrade/tests/exchange/test_exchange_bittrex.py +++ b/freqtrade/tests/exchange/test_exchange_bittrex.py @@ -1,9 +1,8 @@ -# pragma pylint: disable=missing-docstring,C0103 +# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument -import pytest from unittest.mock import MagicMock +import pytest from requests.exceptions import ContentDecodingError - from freqtrade.exchange.bittrex import Bittrex import freqtrade.exchange.bittrex as btx @@ -88,8 +87,7 @@ class FakeBittrex(): 'PricePerUnit': 1, 'Quantity': 1, 'QuantityRemaining': 1, - 'Closed': True - }, + 'Closed': True}, 'message': 'lost'} def fake_cancel_order(self, uuid): @@ -211,24 +209,18 @@ def test_exchange_bittrex_get_ticker(): def test_exchange_bittrex_get_ticker_bad(): wb = make_wrap_bittrex() fb = FakeBittrex() - fb.result = {'success': True, - 'result': {'Bid': 1, 'Ask': 0}} # incomplete result + fb.result = {'success': True, 'result': {'Bid': 1, 'Ask': 0}} # incomplete result with pytest.raises(ContentDecodingError, match=r'.*Got invalid response from bittrex params.*'): wb.get_ticker('BTC_ETH') - fb.result = {'success': False, - 'message': 'gone bad' - } + fb.result = {'success': False, 'message': 'gone bad'} with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'): wb.get_ticker('BTC_ETH') - fb.result = {'success': True, - 'result': {}} # incomplete result + fb.result = {'success': True, 'result': {}} # incomplete result with pytest.raises(ContentDecodingError, match=r'.*Got invalid response from bittrex params.*'): wb.get_ticker('BTC_ETH') - fb.result = {'success': False, - 'message': 'gone bad' - } + fb.result = {'success': False, 'message': 'gone bad'} with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'): wb.get_ticker('BTC_ETH') diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index f5db4d037..368682bad 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -1,28 +1,19 @@ -# pragma pylint: disable=missing-docstring,W0212 +# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103 import logging import math -import pandas as pd -import pytest from unittest.mock import MagicMock +import pandas as pd from freqtrade import exchange, optimize from freqtrade.exchange import Bittrex from freqtrade.optimize import preprocess from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe import freqtrade.optimize.backtesting as backtesting -from freqtrade.strategy.strategy import Strategy -@pytest.fixture -def default_strategy(): - strategy = Strategy() - strategy.init({'strategy': 'default_strategy'}) - return strategy - - -def trim_dictlist(dl, num): +def trim_dictlist(dict_list, num): new = {} - for pair, pair_data in dl.items(): + for pair, pair_data in dict_list.items(): new[pair] = pair_data[num:] return new diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 8a18b58e3..646959669 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -1,4 +1,4 @@ -# pragma pylint: disable=missing-docstring,W0212 +# pragma pylint: disable=missing-docstring, protected-access, C0103 import os import logging @@ -55,8 +55,7 @@ def test_load_data_30min_ticker(default_conf, ticker_history, mocker, caplog): assert os.path.isfile(file) is True assert ('freqtrade.optimize', logging.INFO, - 'Download the pair: "BTC_ETH", Interval: 30 min' - ) not in caplog.record_tuples + 'Download the pair: "BTC_ETH", Interval: 30 min') not in caplog.record_tuples _clean_test_file(file) @@ -72,8 +71,7 @@ def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog): assert os.path.isfile(file) is True assert ('freqtrade.optimize', logging.INFO, - 'Download the pair: "BTC_ETH", Interval: 5 min' - ) not in caplog.record_tuples + 'Download the pair: "BTC_ETH", Interval: 5 min') not in caplog.record_tuples _clean_test_file(file) @@ -89,8 +87,7 @@ def test_load_data_1min_ticker(default_conf, ticker_history, mocker, caplog): assert os.path.isfile(file) is True assert ('freqtrade.optimize', logging.INFO, - 'Download the pair: "BTC_ETH", Interval: 1 min' - ) not in caplog.record_tuples + 'Download the pair: "BTC_ETH", Interval: 1 min') not in caplog.record_tuples _clean_test_file(file) @@ -106,8 +103,7 @@ def test_load_data_with_new_pair_1min(default_conf, ticker_history, mocker, capl assert os.path.isfile(file) is True assert ('freqtrade.optimize', logging.INFO, - 'Download the pair: "BTC_MEME", Interval: 1 min' - ) in caplog.record_tuples + 'Download the pair: "BTC_MEME", Interval: 1 min') in caplog.record_tuples _clean_test_file(file) @@ -173,8 +169,7 @@ def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog): _clean_test_file(file1_5) assert ('freqtrade.optimize.__init__', logging.INFO, - 'Failed to download the pair: "BTC-MEME", Interval: 1 min' - ) in caplog.record_tuples + 'Failed to download the pair: "BTC-MEME", Interval: 1 min') in caplog.record_tuples def test_download_backtesting_testdata(default_conf, ticker_history, mocker): diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 986f3f8f0..34d92b43e 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103 +# pragma pylint: disable=unused-argument import re from datetime import datetime from random import randint diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 79f045a6d..8a2a21f41 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -1,14 +1,7 @@ -import json +# pragma pylint: disable=missing-docstring, protected-access, C0103 + import logging -import pytest from freqtrade.strategy.strategy import Strategy -from freqtrade.analyze import parse_ticker_dataframe - - -@pytest.fixture -def result(): - with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: - return parse_ticker_dataframe(json.load(data_file)) def test_sanitize_module_name(): @@ -28,8 +21,6 @@ def test_search_strategy(): def test_strategy_structure(): assert hasattr(Strategy, 'init') - assert hasattr(Strategy, 'minimal_roi') - assert hasattr(Strategy, 'stoploss') assert hasattr(Strategy, 'populate_indicators') assert hasattr(Strategy, 'populate_buy_trend') assert hasattr(Strategy, 'populate_sell_trend') diff --git a/freqtrade/tests/test_acl_pair.py b/freqtrade/tests/test_acl_pair.py index 7c42c676e..b70596091 100644 --- a/freqtrade/tests/test_acl_pair.py +++ b/freqtrade/tests/test_acl_pair.py @@ -1,3 +1,5 @@ +# pragma pylint: disable=missing-docstring,C0103 + from freqtrade.main import refresh_whitelist, gen_pair_whitelist # whitelist, blacklist, filtering, all of that will @@ -73,16 +75,9 @@ def get_market_summaries(): def get_health(): - return [{'Currency': 'ETH', - 'IsActive': True - }, - {'Currency': 'TKN', - 'IsActive': True - }, - {'Currency': 'BLK', - 'IsActive': True - } - ] + return [{'Currency': 'ETH', 'IsActive': True}, + {'Currency': 'TKN', 'IsActive': True}, + {'Currency': 'BLK', 'IsActive': True}] def get_health_empty(): diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index 516bb3607..2804217b4 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -1,10 +1,8 @@ -# pragma pylint: disable=missing-docstring,W0621 +# pragma pylint: disable=missing-docstring, C0103 import datetime -import json from unittest.mock import MagicMock import arrow -import pytest from pandas import DataFrame import freqtrade.tests.conftest as tt # test tools @@ -14,12 +12,6 @@ from freqtrade.analyze import (get_signal, parse_ticker_dataframe, from freqtrade.strategy.strategy import Strategy -@pytest.fixture -def result(): - with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: - return parse_ticker_dataframe(json.load(data_file)) - - def test_dataframe_correct_columns(result): assert result.columns.tolist() == \ ['close', 'high', 'low', 'open', 'date', 'volume'] diff --git a/freqtrade/tests/test_dataframe.py b/freqtrade/tests/test_dataframe.py index f9230a03f..9af42a30e 100644 --- a/freqtrade/tests/test_dataframe.py +++ b/freqtrade/tests/test_dataframe.py @@ -1,5 +1,6 @@ -import pandas +# pragma pylint: disable=missing-docstring, C0103 +import pandas import freqtrade.optimize from freqtrade import analyze diff --git a/freqtrade/tests/test_fiat_convert.py b/freqtrade/tests/test_fiat_convert.py index 2d112f921..7d0acfc91 100644 --- a/freqtrade/tests/test_fiat_convert.py +++ b/freqtrade/tests/test_fiat_convert.py @@ -1,4 +1,5 @@ -# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103 +# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, +# pragma pylint: disable=protected-access, C0103 import time from unittest.mock import MagicMock @@ -47,16 +48,19 @@ def test_fiat_convert_is_supported(): def test_fiat_convert_add_pair(): fiat_convert = CryptoToFiatConverter() - assert len(fiat_convert._pairs) == 0 + pair_len = len(fiat_convert._pairs) + assert pair_len == 0 fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0) - assert len(fiat_convert._pairs) == 1 + pair_len = len(fiat_convert._pairs) + assert pair_len == 1 assert fiat_convert._pairs[0].crypto_symbol == 'BTC' assert fiat_convert._pairs[0].fiat_symbol == 'USD' assert fiat_convert._pairs[0].price == 12345.0 fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2) - assert len(fiat_convert._pairs) == 2 + pair_len = len(fiat_convert._pairs) + assert pair_len == 2 assert fiat_convert._pairs[1].crypto_symbol == 'BTC' assert fiat_convert._pairs[1].fiat_symbol == 'EUR' assert fiat_convert._pairs[1].price == 13000.2 @@ -95,7 +99,8 @@ def test_fiat_convert_get_price(mocker): fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='US Dollar') # Check the value return by the method - assert len(fiat_convert._pairs) == 0 + pair_len = len(fiat_convert._pairs) + assert pair_len == 0 assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0 assert fiat_convert._pairs[0].crypto_symbol == 'BTC' assert fiat_convert._pairs[0].fiat_symbol == 'USD' diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 8ac06ee69..ba7f8108f 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -1,4 +1,4 @@ -# pragma pylint: disable=missing-docstring,C0103 +# pragma pylint: disable=missing-docstring, C0103 import copy import logging from unittest.mock import MagicMock @@ -325,27 +325,31 @@ def test_handle_overlpapping_signals(default_conf, ticker, mocker): # Buy and Sell triggering, so doing nothing ... trades = Trade.query.all() - assert len(trades) == 0 + nb_trades = len(trades) + assert nb_trades == 0 # Buy is triggering, so buying ... mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) create_trade(0.001, int(default_conf['ticker_interval'])) trades = Trade.query.all() - assert len(trades) == 1 + nb_trades = len(trades) + assert nb_trades == 1 assert trades[0].is_open is True # Buy and Sell are not triggering, so doing nothing ... mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (False, False)) assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is False trades = Trade.query.all() - assert len(trades) == 1 + nb_trades = len(trades) + assert nb_trades == 1 assert trades[0].is_open is True # Buy and Sell are triggering, so doing nothing ... mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, True)) assert handle_trade(trades[0], int(default_conf['ticker_interval'])) is False trades = Trade.query.all() - assert len(trades) == 1 + nb_trades = len(trades) + assert nb_trades == 1 assert trades[0].is_open is True # Sell is triggering, guess what : we are Selling! @@ -468,7 +472,8 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mo assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all() - assert len(trades) == 0 + nb_trades = len(trades) + assert nb_trades == 0 def test_handle_timedout_limit_buy(mocker): diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index b635a5128..401de7acb 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -1,4 +1,4 @@ -# pragma pylint: disable=missing-docstring +# pragma pylint: disable=missing-docstring, C0103 import os import pytest from sqlalchemy import create_engine @@ -12,7 +12,7 @@ def test_init_create_session(default_conf, mocker): # Check if init create a session init(default_conf) assert hasattr(Trade, 'session') - assert type(Trade.session).__name__ is 'Session' + assert 'Session' in type(Trade.session).__name__ def test_init_dry_run_db(default_conf, mocker):