tenacity: 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 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:
- Reach in to the method signature and mock out the default value - seems a bit messy.
- 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).
- Make a change to the signature of
BaseRetrying.__init__()to defaultsleeptoNone, and then default this tonap.sleepinside 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?
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 2
- Comments: 19 (12 by maintainers)
Commits related to this issue
- Replace nap.sleep with a method to allow mocking after import Fixes #228 — committed to BATS/tenacity by asqui 4 years ago
- Replace nap.sleep with a method to allow mocking after import Fixes #228 — committed to asqui/tenacity by asqui 4 years ago
- Replace nap.sleep with a method to allow mocking after import Fixes #228 — committed to asqui/tenacity by asqui 4 years ago
- Replace nap.sleep with a method to allow mocking after import Fixes #228 — committed to asqui/tenacity by asqui 4 years ago
- Merge branch 'master' into mockable-sleep-#228 — committed to asqui/tenacity by asqui 4 years ago
- Merge branch 'master' into mockable-sleep-#228 — committed to asqui/tenacity by mergify[bot] 4 years ago
- Replace nap.sleep with a method to allow mocking after import (#236) Fixes #228 Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> — committed to jd/tenacity by asqui 4 years ago
It was a little bit tricky for me to navigate this thread to find exactly what I needed to mock. here’s the easy answer:
Patching
tenacity.nap.time.sleepdidn’t stop tenacity from retrying for me.Here’s what worked for me (pytest):Update: here’s what worked for me on tenacity 8.2.2 (pytest):
@jd you mean that literally everyone who is serious about their project (and thus writes unit tests) has to subclass Retrying? Forking tenacity would be less work overall…
Hmmm, so just to be clear, the change being talked about here is to change the definition of
nap.sleepfromsleep = time.sleeptodef sleep(seconds): time.sleep(seconds)in order to facilitate easier mocking, in a single place, that works for a variety of usage scenarios.It’s a pretty minor change, with a major benefit for end-to-end testability of code using the library.
Is there a specific aspect of this change that you think might be a pain to maintain in the long run? If not, then I’m failing to see the reasons behind not wanting to merge such a change.
That’s an excellent question to ask. I don’t think there is much. I’m just trying to be really suspicious about changes that are not useful and might be a pain to maintain in the long run. That’s part of my job here. 😄
@jd You said you’re “not really excited about the change”. It’s definitely true that you can work around this already in various ways, but I just think it would be better if you didn’t need to.
In my particular scenario I’m trying to standardise a myriad of retry approaches in a large codebase to all use tenacity, and convince other developers that this is worthwhile. The need to apply workarounds like this in order to be able to test is a barrier to adoption of the library by other developers.
Forking the library and applying the patch I’m suggesting is also an approach, but again carries more overhead than having this in the library as standard, and available to all users of tenacity without each needing to visit this hurdle and come up with a mitigation approach. (e.g. https://github.com/openstack/cinder/blob/307fccea4cc9c6496334b0fe137206cb48499bd5/cinder/utils.py#L56 as discussed earlier in the comment trail)
So to summarise I think there are definite advantages to applying this change to the library, and I can see no downsides.
What are the disadvantages of this change which are making you “not really excited” about making it?