scotscotmcc
scotscotmcc

Reputation: 3123

Mock date.today() but leave other date methods alone

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

Answers (3)

User051209
User051209

Reputation: 2548

Description of changes to the file main.py

I have found a possible solution by the modification of the import in your production code (main.py):

  1. instead of import 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
  1. to reflect the changes in the import, in the code the invocation of the function strptime() has become datetime.datetime.strptime() instead datetime.strptime()
  2. furthermore the invocation of the function today() has become datetime.date.today() instead date.today()

Description of changes to the file 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:

The new code

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

darek_kce
darek_kce

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

Temunel
Temunel

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

Related Questions