Обезьяний патч datetime.now() во всем приложении Django с помощью unittest.mock
У меня есть проект Django с такой структурой файлов:
.
├── 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
В моем views.py
есть вызов datetime.now()
:
from datetime import datetime
def time_now():
return datetime.now().strftime("%Y-%m-%d %H")
В child_app/my_calendar.py
у меня есть еще один вызов datetime.now()
:
from datetime import datetime
def another_method():
return datetime.now().strftime("%Y-%m-%d %H")
Это простой пример, но в реальной жизни это приложение Django имеет множество дочерних приложений с большим количеством вызовов datetime.now()
, и я пытаюсь написать тест, чтобы высмеять их все. Глядя на этот ответ, я могу использовать его для высмеивания datetime.now()
в модуле:
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")
Этот тест проходит, и я могу сделать то же самое для другого модуля:
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")
Этот тест также проходит. Теперь у меня гораздо больше модулей, и писать отдельный декоратор для каждого из них - мучение и, в конечном счете, невыполнимая задача. Как я могу по-обезьяньи исправить каждый datetime.now()
во всем приложении Django (здесь app
) DRY образом? Я не могу этого сделать:
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")
Причина:
AttributeError: <module 'app' from 'mock_django/app/__init__.py'> does
not have the attribute 'datetime'
Теперь я не могу изменить фактический код и могу изменить только тесты, но даже если я добавлю from datetime import datetime
к mock_django/app/__init__.py
, тесты не пройдут.
Я пытался использовать 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")
Но это дает другую ошибку:
TypeError: cannot set 'now' attribute of immutable type 'datetime.datetime'
Вы можете использовать pkgutil.walk_packages
для поиска подмодулей и затем рекурсивно исправлять их:
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
Использование:
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")