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?





Comments:

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()