Reputation: 3123
I am trying to test some python code that involves setting/comparing dates, and so I am trying to leverage unittest.mock
in my testing (using pytest
). The current problem I'm hitting is that using patch
appears to override all the other methods for the patched class (datetime.date
) and so causes other errors because my code is using other methods of the class.
Here is a simplified version of my code.
#main.py
from datetime import date, timedelta, datetime
def date_distance_from_today(dt: str | date) -> timedelta:
if not isinstance(dt, date):
dt = datetime.strptime(dt, "%Y-%m-%d").date()
return date.today() - dt
#tests.py
from datetime import date, timedelta
from unittest.mock import patch
from mock_experiment import main
def test_normal(): # passes fine today, Jan 7
assert main.date_distance_from_today(date(2025, 1, 1)) == timedelta(6)
def test_normal_2(): # passes fine today, Jan 7
assert main.date_distance_from_today("2025-01-01") == timedelta(6)
def test_with_patch_on_date(): # exception thrown
with patch("mock_experiment.main.date") as patch_date:
patch_date.today.return_value = date(2025, 1, 2)
assert main.date_distance_from_today(date(2025, 1, 1)) == timedelta(1)
When I run these tests, the first two pass but the third gets the following exception:
def func1(dt: str | date) -> timedelta:
> if not isinstance(dt, date):
E TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union
This makes sense to me (although not what I want) since I borked out the date
object and turned it into a MagicMock and so it doesn't get handled how I want in this isinstance
call.
I also tried patching date.today
, which also failed as shown below:
def test_with_mock_on_today():
with patch("mock_experiment.main.date.today") as patch_today:
patch_today.return_value = date(2025, 1, 2)
assert main.distance_from_today(date(2025, 1, 1)) == timedelta(1)
Exception
TypeError: cannot set 'today' attribute of immutable type 'datetime.date'
Upvotes: 2
Views: 83
Reputation: 2548
main.py
I have found a possible solution by the modification of the import in your production code (main.py
):
datetime
from the module datetime
I add the import of the module datetime
:# following are my imports
import datetime
from datetime import date, timedelta
# this was your import
#from datetime import date, timedelta, datetime
strptime()
has become datetime.datetime.strptime()
instead datetime.strptime()
today()
has become datetime.date.today()
instead date.today()
tests.py
To remain compliant with the production code I have changed the test method code test_with_patch_on_date()
with the modification of the path of the patch()
:
# this is your patch()
#with patch("mock_experiment.main.date") as patch_date:
# the following is my patch()
with patch('mock_experiment.main.datetime.date') as patch_date:
So the code of main.py
has become the following:
#main.py
# following are my imports
import datetime
from datetime import date, timedelta
# this was your import
#from datetime import date, timedelta, datetime
def date_distance_from_today(dt: str | date) -> timedelta:
if not isinstance(dt, date):
# HERE I HAVE USED datetime.datetime.strptime() instead datetime.strptime()
dt = datetime.datetime.strptime(dt, "%Y-%m-%d").date()
# HERE I HAVE USED datetime.date.today() instead date.today()
return datetime.date.today() - dt
while the code of the test file has become:
import unittest
from datetime import date, timedelta
from unittest.mock import patch
from mock_experiment import main
class MyTestCase(unittest.TestCase):
def test_normal(self): # passes fine today, Jan 10
assert main.date_distance_from_today(date(2025, 1, 1)) == timedelta(9)
def test_normal_2(self): # passes fine today, Jan 10
assert main.date_distance_from_today("2025-01-01") == timedelta(9)
def test_with_patch_on_date(self): # exception thrown, but now pass
# this is your patch()
#with patch("mock_experiment.main.date") as patch_date:
# the following is my patch()
with patch('mock_experiment.main.datetime.date') as patch_date:
patch_date.today.return_value = date(2025, 1, 2)
assert main.date_distance_from_today(date(2025, 1, 1)) == timedelta(1)
if __name__ == '__main__':
unittest.main()
With these modification the 3 tests pass and this is the output on my system:
...
----------------------------------------------------------------------
Ran 3 tests in 0.003s
OK
Note. I don't have used pytest
; I have used the module unittest
, so the test functions in my code are methods of the Test Class MyTestCase
.
Upvotes: 1
Reputation: 33
The problem with your code snippet is that you try to mock the whole date
object, but you are using this also as the type in your isinstance
check. It's tricky but the easiest way to make this test working (without going into mocking details) is to reverse isinstance
logic:
def date_distance_from_today(dt: str | date) -> timedelta:
if isinstance(dt, str):
dt = datetime.strptime(dt, "%Y-%m-%d").date()
return date.today() - dt
Then the test should work as expected.
Upvotes: 2
Reputation: 761
You can try this way.
# main.py
from datetime import date, timedelta, datetime
def date_distance_from_today(dt: str | date) -> timedelta:
if isinstance(dt, str):
dt = datetime.strptime(dt, "%Y-%m-%d").date()
return date.today() - dt
# test.py
import datetime
from unittest.mock import patch
from main import date_distance_from_today
def test_normal():
assert date_distance_from_today(datetime.date(2025, 1, 1)) == datetime.timedelta(6)
def test_normal_2():
assert date_distance_from_today("2025-01-01") == datetime.timedelta(6)
class FakeDate(datetime.date):
@classmethod
def today(cls):
return cls(2025, 1, 2)
def test_with_subclass_patch():
with patch("main.date", FakeDate):
delta = date_distance_from_today(datetime.date(2025, 1, 1))
assert delta == datetime.timedelta(days=1)
I hope this will help you a little.
Upvotes: 1