pytest - fake time changes during test - python

I have the following code to set a faketime during tests.
I'ld like to change the time during a test. That is, the test should start at 9:00 for isntance and then continue as if it is 10:00 .
from __future__ import annotations
import datetime
import logging
import pytest
LOGGER = logging.getLogger(__name__)
#pytest.fixture(params=[datetime.datetime(2020, 12, 25, 17, 5, 55)])
def patch_datetime_now(request, monkeypatch):
class mydatetime(datetime.datetime):
#classmethod
def now(cls):
return request.param
class mydate(datetime.date):
#classmethod
def today(cls):
return request.param.date()
monkeypatch.setattr(datetime, "datetime", mydatetime)
monkeypatch.setattr(datetime, "date", mydate)
#pytest.mark.usefixtures("patch_datetime_now")
#pytest.mark.parametrize(
"patch_datetime_now", [(datetime.datetime(2020, 12, 9, 11, 22, 00))], indirect=True
)
def test_update_data():
fakeTime = datetime.datetime.now()
# Do some stuff
# Change the fake time
# Do some other stuff
How can I change the fake time during the test. The 'datetime' is used inside the code tested, so it's not about changing the "fakeTime" variable contents, but about changing the time returned by the datetime mockup.
Maybe I need to change the mocking method completely, I am just sharing my current code.

Following this answer on another question provided by #MrBeanBremen I updated my code like this:
from __future__ import annotations
import datetime
import logging
import pytest
LOGGER = logging.getLogger(__name__)
#pytest.fixture(params=[datetime.datetime(2020, 12, 25, 17, 5, 55)])
def patch_datetime_now(request, monkeypatch):
def _delta(timedelta=None, **kwargs):
""" Moves time fwd/bwd by the delta"""
from datetime import timedelta as td
if not timedelta:
timedelta = td(**kwargs)
request.param += timedelta
class mydatetime(datetime.datetime):
#classmethod
def now(cls):
return request.param
#classmethod
def delta(cls,*args,**kwargs):
_delta(*args,**kwargs)
class mydate(datetime.date):
#classmethod
def today(cls):
return request.param.date()
#classmethod
def delta(cls,*args,**kwargs):
_delta(*args,**kwargs)
monkeypatch.setattr(datetime, "datetime", mydatetime)
monkeypatch.setattr(datetime, "date", mydate)
#pytest.mark.usefixtures("patch_datetime_now")
#pytest.mark.parametrize(
"patch_datetime_now", [(datetime.datetime(2020, 12, 9, 11, 22, 00))], indirect=True
)
def test_update_data():
fakeTime = datetime.datetime.now()
assert fakeTime == datetime.datetime(2020, 12, 9, 11, 22, 00)
datetime.datetime.delta(hours=1,seconds=10)
fakeTime = datetime.datetime.now()
assert fakeTime == datetime.datetime(2020, 12, 9, 12, 22, 10)

Related

Python unit tests - mocking imported class methods

I would like to mock some imported class methods and module functions for my unit tests. I tried several ways to define the mocked values but I don't understand why they are not taken into account.
I wrote some tests following the advices in Python Mocking a function from an imported module.
Here is a piece of code representing the application to test:
from services import myModule1
from services.spec1 import importedClass
class myClass(object):
def __init__(self, param1, param2):
self.param1 = param1
self.param2 = param2
self.param3 = 0
self.param4 = 0
self.myMethod()
def myMethod(self):
newVar = importedClass()
self.param3 = newVar.meth1(self.param2)
calcParam = myModule1.methodMod1(self.param1)
self.param4 = calcParam["keyParam3"]
I would like to unit test myMethod and I need to mock importedClass and myModule1.methodMod1().
Here is a new piece of code that I tried for the tests (previous attempts below):
import unittest
from unittest.mock import patch
from my_module import myClass
class test_myClass(unittest.TestCase):
#patch('my_module.importedClass')
#patch('my_module.myModule1')
def test_myMethod(self, mock_mod1, mock_class):
mock_mod1.methodMod1.return_value = {"keyParam3": 5, "keyParam4": 7}
mock_class.meth1.return_value = 2
test_parameters = (0, 0)
test_res = myClass(*test_parameters)
self.assertEqual(test_res.param3, 2)
self.assertEqual(test_res.param4, 5)
if __name__ == '__main__':
unittest.main()
The mocking has no error but the mocked values are not taken into account.
Previous attempts
What I tried for each of them, using 2 different approaches:
import unittest
from unittest.mock import Mock, patch
from my_module import myClass
import services
class test_myClass(unittest.TestCase):
def setUp(self):
services.spec1 = Mock()
services.spec1.importedClass.meth1.return_value = 2
#patch('services.myModule1')
def test_myMethod(self, my_mock):
my_mock.methodMod1.return_value = {"keyParam3": 5, "keyParam4": 7}
test_parameters = (0, 0)
test_res = myClass(*test_parameters)
self.assertEqual(test_res.param3, 2)
self.assertEqual(test_res.param4, 5)
if __name__ == '__main__':
unittest.main()
The result is that the calculated attributes are not updated and still 0 - so test fails.
I also tried with services = Mock() and defined return values for each part, to regroup each mock in setUp method or in a #patch, but nothing worked.
I also tried with my_module.spec1 = Mock(), to make the function global, or even self.spec1 = Mock() to make it very local to the test's context (if I understood correctly the differences, this is something I'm not really sure neither) but nothing worked.
To mock just the method:
class test_myClass(unittest.TestCase):
# #patch('my_module.importedClass') # Change this
#patch('my_module.importedClass.meth1') # to this
#patch('my_module.myModule1')
# def test_myMethod(self, mock_mod1, mock_class): # Change this
def test_myMethod(self, mock_mod1, mock_class_meth1): # to this
mock_mod1.methodMod1.return_value = {"keyParam3": 5, "keyParam4": 7}
# mock_class.meth1.return_value = 2 # Change this
mock_class_meth1.return_value = 2 # to this
test_parameters = (0, 0)
test_res = myClass(*test_parameters)
self.assertEqual(test_res.param3, 2)
self.assertEqual(test_res.param4, 5)
To mock the class and its method:
https://docs.python.org/3/library/unittest.mock-examples.html#mocking-classes
class test_myClass(unittest.TestCase):
#patch('my_module.importedClass')
#patch('my_module.myModule1')
def test_myMethod2(self, mock_mod1, mock_class):
mock_mod1.methodMod1.return_value = {"keyParam3": 5, "keyParam4": 7}
# mock_class.meth1.return_value = 2 # Change this
mock_class_instance = mock_class.return_value # to these
mock_class_instance.meth1.return_value = 2 # two lines
test_parameters = (0, 0)
test_res = myClass(*test_parameters)
self.assertEqual(test_res.param3, 2)
self.assertEqual(test_res.param4, 5)

Python different mocking behaviour depending the code style

I found a strange behavior of mocking. Could you please explain me where is the pitfall?
Searching on the net for examples for mocking i found the above piece of code, which works.
But if i rewrite it as unittest.TestCase subclass, the mocking does not works.
import datetime
from unittest.mock import Mock
tuesday = datetime.datetime(year=2019, month=1, day=1)
saturday = datetime.datetime(year=2019, month=1, day=5)
def is_weekday():
today = datetime.datetime.today()
print( "Today is %d/%d/%d"%(today.day, today.month, today.year))
return (0 <= today.weekday() < 5)
datetime = Mock()
datetime.datetime.today.return_value = tuesday
assert is_weekday()
datetime.datetime.today.return_value = saturday
assert not is_weekday()
The above code will produce as expected the following
Today is 1/1/2019
Today is 5/1/2019
I rewrote the above code as subclass of unittest.TestCase
import unittest
import datetime
from unittest.mock import Mock
tuesday = datetime.datetime(year=2019, month=1, day=1)
saturday = datetime.datetime(year=2019, month=1, day=5)
def is_weekday():
today = datetime.datetime.today()
return (0 <= today.weekday() < 5)
class MyTestCase(unittest.TestCase):
def test_something(self):
datetime = Mock()
datetime.datetime.today.return_value = tuesday
assert is_weekday()
datetime.datetime.today.return_value = saturday
assert not is_weekday()
if __name__ == '__main__':
unittest.main()
But now the output is different
Today is 16/7/2021
Today is 16/7/2021
Python is lexically scoped; is_weekday uses a global name datetime, not the local name datetime you assigned the Mock to.
class MyTestCase(unittest.TestCase):
def test_something(self):
global datetime
old_datetime = datetime
datetime = Mock()
datetime.datetime.today.return_value = tuesday
assert is_weekday()
datetime.datetime.today.return_value = saturday
assert not is_weekday()
datetime = old_datetime
Better, though, would be to use unittest.mock.patch
from unittest.mock import patch
class MyTestCase(unittest.TestCase):
#patch('datetime.datetime')
def test_something(self, mock_dt):
mock_dt.today.return_value = tuesday
assert is_weekday()
mock_dt.today.return_value = saturday
assert not is_weekday()

How to unit test a method that contains a database call in Python

I want to unit test a method that includes a database (SQL Server) call.
I don't want the test to connect to the actual database.
I use unittest for testing, I have done some research and it seems that Mocking could do the trick but not sure about the syntax.
The select statement on the code below returns some integers. I guess that mocking will target the "cursor.execute" and "cursor.fetchall()" parts of the code.
from databaselibrary.Db import Db
class RandomClass():
def __init__(self, database):
self.database = database # Main DB for inserting data
def check_file_status(self, trimmed_file_data, file_date):
cursor = self.database.cursor()
cursor.execute(f"""SELECT DISTINCT query_id
FROM wordcloud_count
WHERE date = '{file_date}'""")
queries_in_DB = set(row.query_id for row in cursor.fetchall())
queries_in_file = set(trimmed_file_data.keys())
if queries_in_DB == queries_in_file:
return False
return True
def run(self):
print("Hello")
if __name__ == "__main__":
connection_string = 'sql://user:password#server/database'
database = Db(connection_string, autocommit=True)
random = RandomClass(database)
random.run()
The test class could look like that:
import unittest
from unittest.mock import Mock, patch
from project.RandomClass import RandomClass
from datetime import datetime
class testRandomClass(unittest.TestCase):
def setUp(self):
self.test_class = RandomClass("don't want to put actual database here")
#patch("project.RandomClass.check_file_status",return_value={123, 1234})
def test_check_file_status(self):
keys = {'1234':'2','123':'1','111':'5'}
result = self.test_class.check_file_status(keys, datetime(1900, 1, 1, 23, 59, 59))
self.assertTrue(result)
You should mock the db connection object, and the cursor. Then, set the return value of the cursor to return the expected value. I've tested the below code, and used class Row to mock the rows returned from fetchall call:
import unittest
from unittest.mock import MagicMock
from datetime import datetime
from project.RandomClass import RandomClass
class Row(object):
def __init__(self, x):
self.query_id = x
class testRandomClass(unittest.TestCase):
def setUp(self):
dbc = MagicMock(name="dbconn")
cursor = MagicMock(name="cursor")
cursor.fetchall.return_value = [Row(1), Row(2)]
dbc.cursor.return_value = cursor
self.test_class = RandomClass(dbc)
def test_check_file_status(self):
keys = {'1234': '2', '123': '1', '111': '5'}
result = self.test_class.check_file_status(keys, datetime(1900, 1, 1, 23, 59, 59))
self.assertTrue(result)
Since in your RandomClass you iterate rows and get their query_id, you need to use a class (or a named tuple) as the row objects returned by the mock.
You should create the row objects you expected, and set them as the return value of fetchall.

assert_called_once_with() with datetime.now() in the call parameter?

I have the following test code to test Decorator.
#mock.patch('a.b.c.KafkaProducer')
def test_1(self, mocked):
decorator = Decorator(....)
#decorator()
def test():
return 42
test()
start_time = ???
v = {'type': 'batch', 'start_time': start_time.isoformat()}
mocked.return_value.send.assert_called_once_with(value=v)
However, the test always fail because Decorator calls mocked with dictionary parameter with property of start_time assigned to datetime.now(). Is it a way to compare everything except start_time? Or any other way to test the call?
Two practical approaches:
freeze time using https://pypi.org/project/freezegun/
import datetime
from freezegun import freeze_time
from unittest.mock import patch
import my_library
NOT_NOW = datetime.datetime.now()
#freeze_time("2020-01-01")
#patch("my_library.helper")
def test_foo(_helper):
my_library.under_test(NOT_NOW)
# kinda auto-magic
_helper.assert_called_once_with(datetime.datetime.now())
# or more explicitly
_helper.assert_called_once_with(datetime.datetime(2020, 1, 1))
or, evaluate arguments manually
#patch("my_library.helper", return_value=42)
def test_bar(_helper):
my_library.under_test(NOT_NOW)
assert _helper.call_count == 1
assert _helper.call_args[0][0]
assert _helper.call_args[0][0] != NOT_NOW

Monkeypatch imported module from other file with pytest

As the title says. I want to mock an imported function from a module. In this case the module is datetime and I want to mock datetime.datetime.now. I included what I've done without success.
# main.py:
import datetime
# cannot modify
def call_me(func):
return func()
class A:
variable = call_me(datetime.datetime.now)
# conftest.py:
import pytest
import datetime
FAKE_TIME = datetime.datetime.fromisoformat("2020-03-19T03:30:00")
#pytest.fixture(autouse=True)
def patch_datetime_now(monkeypatch):
class mydatetime(datetime.datetime):
#classmethod
def now(cls):
return FAKE_TIME
monkeypatch.setattr('datetime.datetime', mydatetime)
# test_main.py:
import datetime
from main import A
def test_main():
assert A.variable == datetime.datetime.now()
❯ pytest
------------------------------------------------------------------------------------
def test_main():
> assert A.variable == datetime.datetime.now()
E AssertionError: assert datetime.datetime(2020, 3, 19, 21, 32, 39, 861956) == datetime.datetime(2020, 3, 19, 3, 30)
I searched for a workaround for this and only found this question How to monkeypatch python's datetime.datetime.now with py.test?. The code included is just a minimal example of what I need. Basically I have an app who uses sqlalchemy.orm for saving data and a specific model have a default value for the date equal to datetime.datetime.now and I need to modify the date of the default value to be able to test it. The models are defined in other file.
Thank you for your help.

Categories

Resources