2

I have the following code that I'm trying to test:

great_report.py

from retry import retry

@retry((ReportNotReadyException), tries=3, delay=10, backoff=3)
def get_link(self):
    report_link = _get_report_link_from_3rd_party(params)
    if report_link:
        return report_link
    else:
        stats.count("report_not_ready", 1)
        raise ReportNotReadyException

I've got my testing function which mocks _get_report_link_from_3rd_party which tests everything but I don't want this function to actually pause execution during when I run tests..

@mock.patch('repo.great_report._get_report_link_from_3rd_party', return_value=None)
test_get_link_raises_exception(self, mock_get_report_link):
    self.assertRaises(ReportNotReadyException, get_link)

I tried mocking the retry parameters but am running into issues where get_link keeps retrying over and over which causes long build times instead of just raising the exception and continuing. How can I mock the parameters for the @retry call in my test?

Kentor
  • 657
  • 1
  • 10
  • 27

2 Answers2

10

As hinted here, an easy way to prevent the actual sleeping is by patching the time.sleep function. Here is the code that did that for me:

@patch('time.sleep', side_effect = lambda _: None)
Community
  • 1
  • 1
Tzach
  • 12,889
  • 11
  • 68
  • 115
5

There is no way to change decorators parameters after load the module. Decorators decorate the original function and change it at the module load time.

First I would like encourage you to change your design a little to make it more testable.

If you extract the body of get_link() method test the new method and trust retry decorator you will obtain your goal.

If you don't want add a new method to your class you can use a config module that store variables that you use when call retry decorator. After that you can use two different module for testing and production.

The last way is the hacking way where you replace retry.api.__retry_internal by a your version that invoke the original one by changing just the variables:

import unittest
from unittest.mock import *
from pd import get_link, ReportNotReadyException

import retry
orig_retry_internal = retry.api.__retry_internal
def _force_retry_params(new_tries=-1, new_delay=0, new_max_delay=None, new_backoff=1, new_jitter=0):
    def my_retry_internals(f, exceptions, tries, delay, max_delay, backoff, jitter, logger):
        # call original __retry_internal by new parameters
        return orig_retry_internal(f, exceptions, tries=new_tries, delay=new_delay, max_delay=new_max_delay,
                                   backoff=new_backoff, jitter=new_jitter, logger=logger)
    return my_retry_internals


class MyTestCase(unittest.TestCase):
    @patch("retry.api.__retry_internal", side_effect=_force_retry_params(new_tries=1))
    def test_something(self, m_retry):
        self.assertRaises(ReportNotReadyException, get_link, None)

IMHO you should use that hacking solution only if you are with the back on the wall and you have no chance to redesign you code to make it more testable. The internal function/class/method can change without notice and your test can be difficult to maintain in the future.

Michele d'Amico
  • 22,111
  • 8
  • 69
  • 76