How to stub time.sleep() in Python unit testing - python

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?

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

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

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 =

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

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

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)
common demo: please see the freezegun README
pytest demo: Gist fake sleep function fixture

Related

Unittest mock: return function which receives one pytest fixture and one argument

I'm creating some unit tests and using pytest fixtures in combination with some unittest.mock patch.object calls.
I would like to reuse a function that is called by some of my tests. It makes use of a pytest fixture (specified as the first "argument" of the function) and it requires an additional argument. It looks something like this:
import pandas as pd
import pytest
import os
from unittest.mock import patch
#pytest.fixture()
def rootdir():
return os.path.dirname(os.path.abspath(__file__))
def my_mock_ret(rootdir, number):
print(f"{rootdir}_{number}")
return f"{rootdir}_{number}"
def test_simple(rootdir):
a = pd.DataFrame()
with patch.object(a, 'to_csv', lambda x: my_mock_ret(rootdir, x)) as _:
a.to_csv(rootdir)
The tricky part is how to pass the number argument to my_mock_ret while also being able to access the rootdir fixture from inside it.
I've tried this way using lambda but it does not work.
Edit
It works if y put my_mock_ret inside test_simple, but I don't want to do that because I want to reuse my_mock_ret for several other tests:
import pandas as pd
import pytest
import os
from unittest.mock import patch
#pytest.fixture()
def rootdir():
return os.path.dirname(os.path.abspath(__file__))
def test_simple(rootdir):
def my_mock_ret(number):
print(f"{rootdir}_{number}")
return f"{rootdir}_{number}"
a = pd.DataFrame()
with patch.object(a, 'to_csv', my_mock_ret) as _:
a.to_csv(rootdir)
What you need right here, is the factory pattern:
import pandas as pd
import pytest
import os
from unittest.mock import patch
#pytest.fixture()
def rootdir():
return os.path.dirname(os.path.abspath(__file__))
#pytest.fixture()
def my_mock_ret(rootdir):
def _my_mock_ret(number):
print(f"{rootdir}_{number}")
return f"{rootdir}_{number}"
return _my_mock_ret
def test_simple(my_mock_ret, rootdir):
a = pd.DataFrame()
with patch.object(a, 'to_csv', my_mock_ret) as _:
a.to_csv(rootdir)
here my_mock_ret will create a function, which captures rootdir, and can take the number argument.

Why is my constant mock with python unit test not working

I use some mocking code to mock a constant in python. I read the docs over and over again but I don't see what I do wrong:
from unittest import TestCase, mock
from sync_servicenowjira.adapter_sn_to_jira import adapt_sn_to_jira
from sync_servicenowjira.constants import DOWNLOAD_PATH
from sync_servicenowjira.dao_service_now import get_service_now_tickets
# #mock.patch('sync_servicenowjira.constants.DOWNLOAD_PATH', './test_files/')
class Test(TestCase):
def test_adapt_sn_to_jira(self):
with mock.patch('sync_servicenowjira.constants.DOWNLOAD_PATH', './test_files/'):
test = DOWNLOAD_PATH
print(test)
sn_tickets = get_service_now_tickets(True)
jira_tickets = adapt_sn_to_jira(sn_tickets)
self.assertIsInstance(jira_tickets, list)
self.assertGreater(len(jira_tickets), 1)

Check if pytest fixture is called once during testing

Does pytest provides functionality like unittest.mock to check if the mock was actually called once(or once with some parameter)?
Sample Source code:
my_package/my_module.py
from com.abc.validation import Validation
class MyModule:
def __init__(self):
pass
def will_call_other_package(self):
val = Validation()
val.do()
def run(self):
self.will_call_other_package()
Sample test code for the above source code:
test_my_module.py
import pytest
from pytest_mock import mocker
from my_package.my_module import MyModule
#pytest.fixture
def mock_will_call_other_package(mocker):
mocker.patch('my_package.my_module.will_call_other_package')
#pytest.mark.usefixtures("mock_will_call_other_package")
class TestMyModule:
def test_run(self):
MyModule().run()
#check `will_call_other_package` method is called.
#Looking for something similar to what unittest.mock provide
#mock_will_call_other_package.called_once
If you want to use a fixture that does the patching, you can move the patching into a fixture:
import pytest
from unittest import mock
from my_package.my_module import MyModule
#pytest.fixture
def mock_will_call_other_package():
with mock.patch('my_package.my_module.will_call_other_package') as mocked:
yield mocked
# the mocking will be reverted here, e.g. after the test
class TestMyModule:
def test_run(self, mock_will_call_other_package):
MyModule().run()
mock_will_call_other_package.assert_called_once()
Note that you have to use the fixture parameter in the test. Just using #pytest.mark.usefixtures will not give you access to the mock itself. You can still use it to be effective in all tests in the class, if you don't need to access the mock in all tests (or use autouse=True in the fixture).
Also note that you don't need pytest-mock here - but as mentioned by #hoefling, using it makes the fixture better readable, because you don't need the with clause :
#pytest.fixture
def mock_will_call_other_package(mocker):
yield mocker.patch('my_package.my_module.will_call_other_package')
As an aside: you don't need to import mocker. Fixtures are looked up by name, and available automatically if the respective plugin is installed.
You could try this:
import pytest
from my_package.my_module import MyModule
def test_run(mocker):
mocker.patch('my_package.my_module.will_call_other_package')
MyModule().run()
mock_will_call_other_package.assert_called_once()
First of all, you may not need the burden of an external library such as pytest_mock, because pytest already got you covered using the integration with unittest.
You also do not need to use the usefixtures because whenever you need a fixture, you just receive it in your test method.
An ideal scenario based on your own code would look similar to this:
import pytest
from unittest.mock import patch
from com.abc.validation import Validation
class MyModule:
def __init__(self):
pass
def will_call_other_package(self):
val = Validation()
val.do()
def run(self):
self.will_call_other_package()
#pytest.fixture
def call_other_module():
with patch("my_package.my_module.MyModule.will_call_other_package") as _patched:
yield _patched
class TestMyModule:
def test_run_will_call_other_package(self, call_other_module):
call_other_module.assert_not_called()
obj = MyModule()
call_other_module.assert_not_called()
obj.run()
call_other_module.assert_called_once()
And also if you want to make sure that you did infact patch the target MyModule.will_call_other_package, modify your test like this:
class TestMyModule:
def test_run_will_call_other_package(self, call_other_module):
call_other_module.assert_not_called()
obj = MyModule()
call_other_module.assert_not_called()
obj.run()
call_other_module.assert_called_once()
assert False, (MyModule.will_call_other_package, call_other_module)
And you'll see something similar to this:
AssertionError: (<MagicMock name='will_call_other_package' id='140695551841328'>, <MagicMock name='will_call_other_package' id='140695551841328'>)
As you can see the id of both objects are the same, confirming our experiment was successful.

How can I mock a function that isn't called by name?

Suppose I have a decorator that collects all the functions it decorates to be called at some point in the future.
mydecorator.py
class CallLater(object):
funcs = []
def __init__(self, func):
self.funcs.append(func)
#classmethod
def call_now(cls, *args, **kwargs):
for func in cls.funcs:
func(*args, **kwargs)
Then, I have a function in a module, one of which will be saved by my decorator.
mymodule.py
import logging
from mydecorator import CallLater
logging.basicConfig(level=logging.INFO)
#CallLater
def log_a():
logging.info("A")
#CallLater
def log_b():
logging.info("B")
def log_c():
logging.info("C")
Now, if I import mymodule and call CallLater.call_now(), log_a and log_b will be called. But let's say that during testing, I want log_b to be substituted with log_c. I'll try to do the replacement with a mock.
mock_test.py
import logging
import pytest
from mymodule import log_a, log_c
from mydecorator import CallLater
logging.basicConfig(level=logging.INFO)
pytest_plugins = ('pytest_mock',)
def test_mocking(mocker, caplog):
mocker.patch('mymodule.log_b', log_c)
CallLater.call_now()
logs = [rec.message for rec in caplog.records]
assert logs == ["A", "C"]
But when I run pytest, I see that my mock didn't work.
FAILED mock_test.py::test_mocking - AssertionError: assert ['A', 'B'] == ['A', 'C']
I imagine that 'mymodule.log_b' is the wrong mocking target since it's not being invoked as mymodule.log_b(), but I'm not sure what to use instead in this situation. Any advice is appreciated!
This is not possible as such, the problem being that the functions are already assigned to the list at load time. The only way I can see to patch this would be to patch CallLater.funcs directly, which is a bit awkward, because you have to replace log_b by log_c manually - but here it goes:
import logging
from unittest import mock
from mymodule import log_c
from mydecorator import CallLater
logging.basicConfig(level=logging.INFO)
def test_mocking(caplog):
funcs = [log_c if f.__name__ == 'log_b' else f for f in CallLater.funcs]
with mock.patch.object(CallLater, 'funcs', funcs):
CallLater.call_now()
logs = [rec.message for rec in caplog.records]
assert logs == ["A", "C"]
Note that you cannot compare directly to the function (e.g. f == log_b), because log_b is the decorated function, not the function as saved in CallLater.funcs.

Python: issue with building mock function

I'm writing unit tests to validate my project functionalities. I need to replace some of the functions with mock function and I thought to use the Python mock library. The implementation I used doesn't seem to work properly though and I don't understand where I'm doing wrong. Here a simplified scenario:
root/connector.py
from ftp_utils.py import *
def main():
config = yaml.safe_load("vendor_sftp.yaml")
downloaded_files = []
downloaded_files = get_files(config)
for f in downloaded_files:
#do something
root/utils/ftp_utils.py
import os
import sys
import pysftp
def get_files(config):
sftp = pysftp.Connection(config['host'], username=config['username'])
sftp.chdir(config['remote_dir'])
down_files = sftp.listdir()
if down_files is not None:
for f in down_files:
sftp.get(f, os.path.join(config['local_dir'], f), preserve_mtime=True)
return down_files
root/tests/connector_tester.py
import unittest
import mock
import ftp_utils
import connector
def get_mock_files():
return ['digital_spend.csv', 'tv_spend.csv']
class ConnectorTester(unittest.TestCase)
#mock.patch('ftp_utils.get_files', side_effect=get_mock_files)
def test_main_process(self, get_mock_files_function):
# I want to use a mock version of the get_files function
connector.main()
When I debug my test I expect that the get_files function called inside the main of connector.py is the get_mock_files(), but instead is the ftp_utils.get_files(). What am I doing wrong here? What should I change in my code to properly call the get_mock_file() mock?
Thanks,
Alessio
I think there are several problems with your scenario:
connector.py cannot import from ftp_utils.py that way
nor can connector_tester.py
as a habit, it is better to have your testing files under the form test_xxx.py
to use unittest with patching, see this example
In general, try to provide working minimal examples so that it is easier for everyone to run your code.
I modified rather heavily your example to make it work, but basically, the problem is that you patch 'ftp_utils.get_files' while it is not the reference that is actually called inside connector.main() but probably rather 'connector.get_files'.
Here is the modified example's directory:
test_connector.py
ftp_utils.py
connector.py
test_connector.py:
import unittest
import sys
import mock
import connector
def get_mock_files(*args, **kwargs):
return ['digital_spend.csv', 'tv_spend.csv']
class ConnectorTester(unittest.TestCase):
def setUp(self):
self.patcher = mock.patch('connector.get_files', side_effect=get_mock_files)
self.patcher.start()
def test_main_process(self):
# I want to use a mock version of the get_files function
connector.main()
suite = unittest.TestLoader().loadTestsFromTestCase(ConnectorTester)
if __name__ == "__main__":
unittest.main()
NB: what is called when running connector.main() is 'connector.get_files'
connector.py:
from ftp_utils import *
def main():
config = None
downloaded_files = []
downloaded_files = get_files(config)
for f in downloaded_files:
print(f)
connector/ftp_utils.py unchanged.

Categories