How to stub time.sleep() in Python unit testing
Asked Answered
S

7

32

I want to make a stub to prevent time.sleep(..) to sleep to improve the unit test execution time.

What I have is:

import time as orgtime

class time(orgtime):
    '''Stub for time.'''
    _sleep_speed_factor = 1.0

    @staticmethod
    def _set_sleep_speed_factor(sleep_speed_factor):
        '''Sets sleep speed.'''
        time._sleep_speed_factor = sleep_speed_factor


    @staticmethod
    def sleep(duration):
        '''Sleeps or not.'''
        print duration * time._sleep_speed_factor
        super.sleep(duration * time._sleep_speed_factor) 

However, I get the following error on the second code line above (class definition):

TypeError: Error when calling the metaclass bases
module.__init__() takes at most 2 arguments (3 given).

How to fix the error?

Steal answered 3/4, 2014 at 11:53 Comment(1)
you are inheriting from a module not a classSunwise
I
53

You can use mock library in your tests.

import time
from mock import patch

class MyTestCase(...):


     @patch('time.sleep', return_value=None)
     def my_test(self, patched_time_sleep):
          time.sleep(666)  # Should be instant
Isochronal answered 3/4, 2014 at 13:35 Comment(6)
I accepted the answer, but sadly I cannot test it, since mock is not part the components we can include.Steal
mock is an open source Python library available on PyPi and works on every platform. Usually there shouldn't be any practical, legal or any other issues including or embedding such a library.Isochronal
the problem is that I cannot decide what libraries are being used in my projects (at least not add libraries).Steal
That is a social problem within your organization. I suggest you take this decision to your team and explain the benefits of using best-practice unit testing tool to make development more efficient. If they don't believe you you can tell a random dude in Internet told you so :) Because the alternative is ripping out code from Mock library and THAT'S UGLY.Isochronal
see @Jeeppler's answer. Mock is standard nowSherysherye
I get fixture 'patched_time_sleep' not found. Is there an extra line to add to define the fixture?Pansir
I
19

The accepted answer is still valid. However, unittest.mock is since Python 3.3 an official part of the Python standard library.

import time
from unittest import TestCase
from unittest.mock import patch

class TestMyCase(TestCase):

    @patch('time.sleep', return_value=None)
    def test_my_method(self, patched_time_sleep):
        time.sleep(60)  # Should be instant

        # the mock should only be called once
        self.assertEqual(1, patched_time_sleep.call_count)
        # or 
        patched_time_sleep.assert_called_once()

    # alternative version using a context manager
    def test_my_method_alternative(self):
        with patch('time.sleep', return_value=None) as patched_time_sleep:
            time.sleep(60)  # Should be instant

        # the mock should only be called once
        self.assertEqual(1, patched_time_sleep.call_count)
        # or 
        patched_time_sleep.assert_called_once()
Idelson answered 19/6, 2018 at 0:37 Comment(1)
the decorator solution works well even for time.sleep inside all method calls I make, but the context manager solution dont. Any ideia on why this differance happen?Ashelman
R
17

I'm using pytest and have following fixture to monkey patch time.sleep:

import pytest


@pytest.fixture
def sleepless(monkeypatch):

    def sleep(seconds):
        pass

    monkeypatch.setattr(time, 'sleep', sleep)

Then in test which I need to "speedup" the sleep, I just use this fixture:

import time

def test_sleep(sleepless):
    time.sleep(60)

So when you run this test, you will see that it completes in much shorter time:

= 1 passed in 0.02 seconds =
Ruphina answered 22/2, 2019 at 14:46 Comment(3)
How would one replace it when time.sleep is called within the tested module's method?Bartolommeo
instead of calling time.sleep inside the test, call tested module method where you use time.sleep. Note, that if you do from time import sleep in your module, instead of patching time module, you will need to patch your module in line monkey patch.setattr...Ruphina
I figured out a solution. Module A had line from time import sleep. I changed it to just import time then the decorator @patch('time.sleep', return_value=None) worked when testing Module B that included module A.Bartolommeo
A
7

Here's what I did to prevent the test from sleeping:

If I have a module mymodule.py that imports and uses sleep in a function that I want to test:

from time import sleep

def some_func()
    sleep(5)
    # ...do some things

I then have my test import sleep from the module that is using it, like this:

@mock.patch('mymodule.sleep')
def test_some_func(mock_sleep):
    mock_sleep.return_value = None
    # ...continue my test
Approximation answered 2/6, 2022 at 21:56 Comment(1)
Thanks, this helped me fix a unit test that was failing intermittently. I was patching time.sleep instead of mymodule.sleep. While this would work most of the time, I would occasionally pick up some random call from elsewhere and it didn't dawn on me why until I saw this answer.Ci
S
3

What about:

import time
from time import sleep as originalsleep

def newsleep(seconds):
    sleep_speed_factor = 10.0 
    originalsleep(seconds/sleep_speed_factor)

time.sleep = newsleep

This is working for me. I am inlcuding it at the beginning of the test I want to speed up, at the end I set back the original sleep just in case. Hope it helps

Stable answered 5/9, 2014 at 15:39 Comment(0)
D
1

Here is a slightly different variation on the aforementioned answers (allowing arguments to be logged during unit test execution while logging args passed):

I had a quick and dirty back off function like like the following (there's probably several libraries to do this better, but I didn't feel like googling - I guess I ended up here anyhow though).

def backoff(retries_left: int = 3, retries_max: int = 3) -> int:
    '''
        Linear 15 second backoff
    '''
    sleep_time = (retries_max - retries_left) * 15

    if sleep_time > 0:
        logging.warning('Retrying in %d seconds...', sleep_time)
        time.sleep(sleep_time)

    return sleep_time

Here's how I wrote the unit test around the expected values

import unittest
from unittest.mock import patch

import my_app as app

def mock_sleep(sleep_time: int = 0):
    '''
        Mock the sleep function.
    '''
    app.logging.info('time.sleep called with %d seconds', sleep_time)


class TestApp(unittest.TestCase):
    '''
        Test cases for the app module.
    '''
    @patch('time.sleep', mock_sleep)
    def test_backoff(self):
        '''
            Test the backoff function.
        '''
        values = {
            3: 0,
            2: 15,
            1: 30,
            0: 45
        }

        for key, value in values.items():
            self.assertEqual(
                app.backoff(key),
                value
            )

        self.assertEqual(
            app.backoff(retries_left=1, retries_max=5),
            60
        )

Output for the unit tests are like:

2024-05-08 10:06:41,047 WARNING Retrying in 15 seconds...
2024-05-08 10:06:41,047 INFO time.sleep called with 15 seconds
2024-05-08 10:06:41,047 WARNING Retrying in 30 seconds...
2024-05-08 10:06:41,047 INFO time.sleep called with 30 seconds
2024-05-08 10:06:41,047 WARNING Retrying in 45 seconds...
2024-05-08 10:06:41,047 INFO time.sleep called with 45 seconds
2024-05-08 10:06:41,047 WARNING Retrying in 60 seconds...
2024-05-08 10:06:41,047 INFO time.sleep called with 60 seconds
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

Taking this a step further I suppose *args and **kwargs could be referenced instead of a predefined value like in the above mocked function example.

Dana answered 8/5 at 16:14 Comment(0)
D
-2

using freezegun package can help you to do this.

# fake.py
import functools
from datetime import datetime, timedelta
from unittest import mock

from freezegun import freeze_time


def fake_sleep(func):
    freezegun_control = None

    def fake_sleep(seconds):
        nonlocal freezegun_control
        utcnow = datetime.utcnow()
        if freezegun_control is not None:
            freezegun_control.stop()
        freezegun_control = freeze_time(utcnow + timedelta(seconds=seconds))
        freezegun_control.start()

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        with mock.patch('time.sleep', fake_sleep):
            rv = func(*args, **kwargs)

            if freezegun_control is not None:
                freezegun_control.stop()
            return rv

    return wrapper


# test.py
from fake import fake_sleep

import time

@fake_sleep
def test_sleep():
    now = datetime.utcnow()

    for sleep_seconds in range(10):
        for i in range(1, 10):

            time.sleep(sleep_seconds)

            assert datetime.utcnow() - now >= timedelta(
                seconds=i * sleep_seconds)
  1. common demo: please see the freezegun README
  2. pytest demo: Gist fake sleep function fixture
Dropsy answered 14/3, 2018 at 9:47 Comment(3)
A link to a solution is welcome, but please ensure your answer is useful without it: add context around the link so your fellow users will have some idea what it is and why it’s there, then quote the most relevant part of the page you're linking to in case the target page is unavailable. Answers that are little more than a link may be deleted.Cameo
This doesn't answer the question. The question is about patching time.sleep(), freezegun only patches datetime and a few related functions in time, but not time.sleep().Braque
you can get rid of time.sleep, when using with freeze_time(datetime.now() + timedelta(seconds=666)):Dropsy

© 2022 - 2024 — McMap. All rights reserved.