Repository: jd/tenacity

Issue:
Cannot easily mock out sleep function in order to simulate time in tests
Given the level of flexibility of this library, and a slightly elaborate context which I am using it in, it is desirable to be able to write an integration test where the concepts of 'time' (i.e. `time.monotonic`) and 'sleep' (i.e. `tenacity.nap.sleep`) are mocked out so that we can simulate in full fidelity how a retry scenario plays out, without having to actually sleep.

Using the [testfixtures](https://github.com/Simplistix/testfixtures) library and `mock.patch` I'm mocking out 'time' with an object that always returns the same 'virtual' time, and 'sleep' with a function that advances this 'virtual' time.

This approach works pretty well, save for one complication...

Since the signature of `BaseRetrying.__init__()` defaults the `sleep` argument to `tenacity.nap.sleep` this default is bound at import time, making it hard to mock out the sleep function after the fact.

Options I can think of:
1. Reach in to the method signature and mock out the default value - seems a bit messy.
2. Pass in a sleep function in the code under test, then mock this out in the test - I don't like this because it involves modifying production code simply for the sake of the test, and also makes this mocking strategy less portable if I want to use it in other tests (because now I need to mock out sleep at my callsite, rather than inside tenacity).
3. Make a change to the signature of `BaseRetrying.__init__()` to default `sleep` to `None`, and then default this to `nap.sleep` inside the method: `self.sleep = sleep or nap.sleep`.

Option 3 seems the neatest, because now I can just mock out `tenacity.nap.sleep` which is the most intuitive approach, but it does involve a change to the library which is why I wanted to solicit input first before preparing a PR.

Thoughts?





Discussion:
asqui: Ok, I think this branch is ready to merge now.
asqui: @ivanprado : Fascinating failure when I merged your change 78a462eb into this PR branch.

This PR makes a tweak to allow easier mocking of nap.sleep, but the test failures indicate that when using the `Retrying.call()` method (which is deprecated by your commit) `sleep()` is now getting called 24 times rather than 4.

I tweaked the test to invoke using `__call__` (i.e. `retrying(fn)` rather than `retrying.call(fn)` in the spirit of your change) and this resolves the issue, but it suggests that something is very wrong with the backward compatibility for `call()` introduced by your commit.

Smells like we're getting nested layers of retrying, but I can't immediately see how or why that is happening. (Also, if that were the case I'd intuitively expect 4 x 4 = 16 retries, but seems we're getting 24 (= 4 x 6?) retries.)

This failure also hints we might want to (re-)introduce test coverage for the `Retrying.call()` method.
ivanprado: Wow. I have no idea what can be going on. 

To give an idea, we might change this line https://github.com/jd/tenacity/commit/78a462eb71e13698fd0b8643b6c4336291ee013a#diff-03f4d209c0b66034a81979dd67efaa24R435 by:
```py
self(self, *args, **kwargs)
```

But in theory, it is completely equivalent. Could you try just in case it changes anything?
asqui: @ivanprado : The issue was that you are passing `self` as the first argument, which is meant to be the function to be executed by the retryer. That is, the retryer is going to execute _itself_, with retries!

PR #258 raised to resolve this.
jd: @Mergifyio update
mergify[bot]: **Command `update`: success**
> **Branch has been successfully updated**
> 

asqui: Tests are green again now.

Is there anything else required before this PR can be merged?

Patch:
diff --git a/releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml b/releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml
new file mode 100644
index 0000000..0a996e4
--- /dev/null
+++ b/releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml
@@ -0,0 +1,3 @@
+---
+other:
+  - Unit tests can now mock ``nap.sleep()`` for testing in all tenacity usage styles
\ No newline at end of file
diff --git a/tenacity/nap.py b/tenacity/nap.py
index e5bd8dc..83ff839 100644
--- a/tenacity/nap.py
+++ b/tenacity/nap.py
@@ -18,8 +18,14 @@
 
 import time
 
-#: Default sleep strategy.
-sleep = time.sleep
+
+def sleep(seconds):
+    """
+    Sleep strategy that delays execution for a given number of seconds.
+
+    This is the default strategy, and may be mocked out for unit testing.
+    """
+    time.sleep(seconds)
 
 
 class sleep_using_event(object):


Test patch:
diff --git a/tenacity/tests/test_tenacity.py b/tenacity/tests/test_tenacity.py
index 673541c..223b19a 100644
--- a/tenacity/tests/test_tenacity.py
+++ b/tenacity/tests/test_tenacity.py
@@ -1565,5 +1565,50 @@ def reports_deprecation_warning():
         warnings.filters = oldfilters
 
 
+class TestMockingSleep():
+    RETRY_ARGS = dict(
+        wait=tenacity.wait_fixed(0.1),
+        stop=tenacity.stop_after_attempt(5),
+    )
+
+    def _fail(self):
+        raise NotImplementedError()
+
+    @retry(**RETRY_ARGS)
+    def _decorated_fail(self):
+        self._fail()
+
+    @pytest.fixture()
+    def mock_sleep(self, monkeypatch):
+        class MockSleep(object):
+            call_count = 0
+
+            def __call__(self, seconds):
+                self.call_count += 1
+
+        sleep = MockSleep()
+        monkeypatch.setattr(tenacity.nap.time, "sleep", sleep)
+        yield sleep
+
+    def test_call(self, mock_sleep):
+        retrying = Retrying(**self.RETRY_ARGS)
+        with pytest.raises(RetryError):
+            retrying.call(self._fail)
+        assert mock_sleep.call_count == 4
+
+    def test_decorated(self, mock_sleep):
+        with pytest.raises(RetryError):
+            self._decorated_fail()
+        assert mock_sleep.call_count == 4
+
+    def test_decorated_retry_with(self, mock_sleep):
+        fail_faster = self._decorated_fail.retry_with(
+            stop=tenacity.stop_after_attempt(2),
+        )
+        with pytest.raises(RetryError):
+            fail_faster()
+        assert mock_sleep.call_count == 1
+
+
 if __name__ == '__main__':
     unittest.main()