Search code examples
pythonunit-testingmockingtwistedtrial

Mock out time.time() for twisted trial testing


I am using mock==1.0.1 and python version 2.7.3 and am using twisted trial to run tests. Twisted version: 13.2.0

I am writing a mock test for a function with inlineCallbacks decorator. The function uses time() from time module and I wish to mock that as the return value of the function depends on the returned time. And I want to assert the return value.

So, I added a @patch decorator but the mock test is not being executed (doesn't show up in the list of executed tests). Here is the code -

CURRENT_TIME = time.time()
@patch('my_module.time')
@defer.inlineCallbacks
def test_index(self, mock_time):
    mock_time.time.return_value = CURRENT_TIME
    handler = ClassToTest()
    result = yield handler.toTestFunc("")
    assertEqual(result, {"time":CURRENT_TIME, "values":4})

My problem - running the tests by trial test.py runs all other tests except test_index. On commenting out the patch decorator, the test runs and gives out an error. I read Where to patch section but still don't know how to patch time.time() function. How should I mock out time.time() function for the test


Solution

  • Instead of mocking, parameterize.

    from time import time
    
    class ClassToTest(object):
        def __init__(self, time=time):
            self._time = time
    
        def toTestFunc(self, arg):
            return {"time": self._time(), "values": 4}
    
    def test_index(self):
        sut = ClassToTest(lambda: CURRENT_TIME)
        result = self.successResultOf(sut.toTestFunc(""))
        self.assertEqual({"time": CURRENT_TIME, "values": 4}, result)
    

    Monkey-patching globals is sort of the same thing as parameterizing, just not done very well. You don't need to replace the time module for the entire process (or even for all of the code in one module). You just need to replace it for the unit under test. The most straightforward way to do this is to pass it as a parameter.

    There are some other things you probably want to do as well. This applies whether you use mocking and monkey-patching or parameters.

    You want to test that the code works when you haven't monkeyed around with it. After all, that's just how it will run in production, where you actually want it to work.

    from time import time
    
    def test_default_time_function(self):
        sut = ClassToTest()
        before = time()
        result = self.successResultOf(sut.toTestFunc(""))
        after = time()
        # Possibly not ideal, but seems to work okay in practice and I haven't
        # had any better ideas for how to make assertions about `time()`
        self.assertTrue(before <= result["time"] <= after)
    

    Since you're using Twisted, you might also want to know about the IReactorTime.seconds API and the twisted.internet.task.Clock fake.

    from twisted.internet.task import Clock
    
    class ClassToTest(object):
        def __init__(self, reactor):
            self._reactor = reactor
    
        def toTestFunc(self, arg):
            return {"time": self._reactor.seconds(), "values": 4}
    
    def test_index(self):
        when = 123456.7
        reactor = Clock()
        reactor.advance(when)
    
        sut = ClassToTest(reactor)
        result = self.successResultOf(sut.toTestFunc(""))
        self.assertEqual({"time": when, "values": 4}, result)
    

    I didn't bother to supply a default reactor in this case. You could, but explicitly passing the reactor is a pretty good idea in general so I usually don't supply a default.