I have a Django project with this file structure:
.
├── app
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── child_app
│ │ └── my_calendar.py
│ ├── migrations
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── manage.py
└── mock_django
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
In my views.py
, I have a call to datetime.now()
:
from datetime import datetime
def time_now():
return datetime.now().strftime("%Y-%m-%d %H")
And in child_app/my_calendar.py
, I have another call to datetime.now()
:
from datetime import datetime
def another_method():
return datetime.now().strftime("%Y-%m-%d %H")
This is a simple example, but in real life this Django app has a lot of child apps with many calls to datetime.now()
, and I'm trying to write a test to mock all of them. Looking at this answer, I can use this to mock datetime.now()
in a module:
from django.test import TestCase
from unittest import mock
from datetime import datetime
from views import time_now
class TestApp(TestCase):
@mock.patch("views.datetime", wraps=datetime)
def test_datetime_now(self, views_datetime):
views_datetime.now.return_value = datetime(2022, 9, 10, 14)
self.assertEqual(time_now(), "2022-09-10 14")
This test passes, and I can do the same for the other module:
from django.test import TestCase
from unittest import mock
from datetime import datetime
from views import time_now
from child_app.my_calendar import another_method
class TestApp(TestCase):
@mock.patch("views.datetime", wraps=datetime)
@mock.patch("child_app.my_calendar.datetime", wraps=datetime)
def test_datetime_now(self, my_calendar_datetime, views_datetime):
my_calendar_datetime.now.return_value = datetime(2022, 9, 10, 14)
views_datetime.now.return_value = datetime(2022, 9, 10, 14)
self.assertEqual(time_now(), "2022-09-10 14")
self.assertEqual(another_method(), "2022-09-10 14")
This test passes as well. Now I have a lot more modules and it's a pain, and ultimately infeasible, to write a separate decorator for each and every one of them. How can I monkey patch every datetime.now()
in an entire Django app (here app
) in a DRY manner? I can't do this:
from django.test import TestCase
from unittest import mock
from datetime import datetime
from views import time_now
from child_app.my_calendar import another_method
class TestApp(TestCase):
@mock.patch("app.datetime", wraps=datetime)
def test_datetime_now(self, mock_datetime):
mock_datetime.now.return_value = datetime(2022, 9, 10, 14)
self.assertEqual(time_now(), "2022-09-10 14")
self.assertEqual(another_method(), "2022-09-10 14")
Because:
AttributeError: <module 'app' from 'mock_django/app/__init__.py'> does
not have the attribute 'datetime'
Now I can't change the actual code and I can only change the tests, but even if I add from datetime import datetime
to mock_django/app/__init__.py
, the tests will fail.
I tried to use mock.patch.object
:
from django.test import TestCase
from unittest import mock
from datetime import datetime
from views import time_now
from child_app.my_calendar import another_method
class TestApp(TestCase):
@mock.patch.object(datetime, 'now')
def test_datetime_now(self, mock_datetime):
mock_datetime.now.return_value = datetime(2022, 9, 10, 14)
self.assertEqual(time_now(), "2022-09-10 14")
self.assertEqual(another_method(), "2022-09-10 14")
But this gives another error:
TypeError: cannot set 'now' attribute of immutable type 'datetime.datetime'
I can achieve the results I want using time_machine
:
from django.test import TestCase
from datetime import datetime
from views import time_now
from child_app.my_calendar import another_method
import time_machine
@time_machine.travel(datetime(2022, 9, 10, 14))
class TestApp(TestCase):
def test_datetime_now(self):
self.assertEqual(time_now(), "2022-09-10 14")
self.assertEqual(another_method(), "2022-09-10 14")
But I'm wondering if I can do exactly this with unittest.mock
.
CodePudding user response:
You can use pkgutil.walk_packages
to find the submodules and then recursively patch them:
import contextlib
import importlib
import pkgutil
from unittest.mock import DEFAULT, MagicMock, patch
def patch_in_app(app, *, attribute, wraps):
module = importlib.import_module(app)
targets = [
f"{name}.{attribute}"
for _, name, _ in pkgutil.walk_packages(module.__path__, prefix=f"{app}.")
if hasattr(importlib.import_module(name), attribute)
]
def decorator(func):
@functools.wraps(func)
def _func(*args):
with patch_all(*targets, new=MagicMock(wraps=wraps)) as mock:
return func(*args, mock)
return _func
return decorator
@contextlib.contextmanager
def patch_all(target, *targets, new=DEFAULT):
with patch(target, new=new) as mock:
if targets:
with patch_all(*targets, new=mock):
yield mock
else:
yield mock
Usage:
class TestApp(TestCase):
# @mock.patch("app.views.datetime", wraps=datetime) # Change these
# @mock.patch("app.child_app.my_calendar.datetime", wraps=datetime) #
@patch_in_app("app", attribute="datetime", wraps=datetime) # to this
def test_datetime_now(self, mock_datetime):
mock_datetime.now.return_value = datetime(2022, 9, 10, 14)
self.assertEqual(time_now(), "2022-09-10 14")
self.assertEqual(another_method(), "2022-09-10 14")