Próbuję udawać datetime.date.today (), ale nie działa


158

Czy ktoś może mi powiedzieć, dlaczego to nie działa?

>>> import mock
>>> @mock.patch('datetime.date.today')
... def today(cls):
...  return date(2010, 1, 1)
...
>>> from datetime import date
>>> date.today()
datetime.date(2010, 12, 19)

Może ktoś mógłby zasugerować lepszy sposób?



Odpowiedzi:


124

Jest kilka problemów.

Po pierwsze, sposób, w jaki używasz, mock.patchnie jest do końca właściwy. Gdy jest używany jako dekorator, zastępuje daną funkcję / klasę (w tym przypadku datetime.date.today) Mockprzedmiotem tylko w funkcji dekorowanej . Tak więc tylko w twojej today()woli datetime.date.todaybędzie inna funkcja, która nie wydaje się być tym, czego chcesz.

To, czego naprawdę chcesz, wydaje się bardziej podobne do tego:

@mock.patch('datetime.date.today')
def test():
    datetime.date.today.return_value = date(2010, 1, 1)
    print datetime.date.today()

Niestety to nie zadziała:

>>> test()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "build/bdist.macosx-10.6-universal/egg/mock.py", line 557, in patched
  File "build/bdist.macosx-10.6-universal/egg/mock.py", line 620, in __enter__
TypeError: can't set attributes of built-in/extension type 'datetime.date'

Nie udaje się to, ponieważ typy wbudowane Pythona są niezmienne - zobacz tę odpowiedź, aby uzyskać więcej informacji.

W tym przypadku podklasowałbym datetime.date siebie i utworzył odpowiednią funkcję:

import datetime
class NewDate(datetime.date):
    @classmethod
    def today(cls):
        return cls(2010, 1, 1)
datetime.date = NewDate

A teraz możesz zrobić:

>>> datetime.date.today()
NewDate(2010, 1, 1)

13
fajne rozwiązanie, ale niestety powoduje problemy z wytrawianiem.
Baczek

14
Chociaż ta odpowiedź jest dobra, można
udawać datę i godzinę

Jak przywrócić datetimeinstancji do jej pierwotnej wartości? z deepcoppy?
Oleg Belousov

5
O wiele łatwiejsze do zrobienia:patch('mymodule.datetime', Mock(today=lambda: date(2017, 11, 29)))
Victor Gavro

1
O wiele łatwiejsze do zrobienia @patch('module_you_want_to_test.date', Mock( today=Mock(return_value=datetime.date(2017, 11, 29)))).
Jonhy Beebop,

163

Inną opcją jest użycie https://github.com/spulec/freezegun/

Zainstaluj to:

pip install freezegun

I użyj go:

from freezegun import freeze_time

@freeze_time("2012-01-01")
def test_something():

    from datetime import datetime
    print(datetime.now()) #  2012-01-01 00:00:00

    from datetime import date
    print(date.today()) #  2012-01-01

Wpływa również na inne wywołania datetime w wywołaniach metod z innych modułów:

other_module.py:

from datetime import datetime

def other_method():
    print(datetime.now())    

main.py:

from freezegun import freeze_time

@freeze_time("2012-01-01")
def test_something():

    import other_module
    other_module.other_method()

I w końcu:

$ python main.py
# 2012-01-01

13
Bardzo przydatna biblioteka
Shaun

3
Możesz także wypróbować python-libfaketime, jeśli zauważysz, że testy Frezegun działają wolno.
Simon Weber,

Świetna biblioteka, ale niestety nie działa dobrze z Google App Engine NDB / Datastore.
brandones

Uwielbiam to, że „frozenegun” to nazwa biblioteki. Uwielbiam twórców Pythona! :-D
MikeyE

Działa, ale Frezegun wydaje się być powolny, zwłaszcza jeśli masz skomplikowaną logikę z wieloma wywołaniami dla bieżącego czasu.
Andrey Belyak

115

Co jest warte, dokumentacja Mock mówi konkretnie o datetime.date.today i można to zrobić bez konieczności tworzenia fikcyjnej klasy:

https://docs.python.org/3/library/unittest.mock-examples.html#partial-mocking

>>> from datetime import date
>>> with patch('mymodule.date') as mock_date:
...     mock_date.today.return_value = date(2010, 10, 8)
...     mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
...
...     assert mymodule.date.today() == date(2010, 10, 8)
...     assert mymodule.date(2009, 6, 8) == date(2009, 6, 8)
...

2
To naprawdę nie zadziałało dla mnie. Chociaż doceniam trud znalezienia wpisu.
Pradyot

8
co oznacza „mymodule” w funkcji patch?
seufagner

4
Odnalazłem link tutaj w części „Częściowe wyśmiewanie”
Leo C Han

3
@seufagner mymodule jest wyjaśniony w dość mylący sposób na stronie voidspace.org.uk/python/mock/patch.html#where-to-patch . Wygląda na to, że jeśli twój moduł używa, from datetime import dateto jest to nazwa modułu, w którym pojawia from datetime import datesię wezwaniedate.today()
danio

1
Dzięki. Pracowałem! Przykład: with mock.patch ('tests.views.datetime') as mock_date: mock_date.today.return_value = datetime.datetime (2016, 9, 18) mock_date.side_effect = lambda * args, ** kw: date (* args , ** kw)
Latrova

36

Wydaje mi się, że trochę się spóźniłem na to, ale myślę, że głównym problemem jest to, że bezpośrednio łatasz datetime.date.today i zgodnie z dokumentacją jest to błędne.

Na przykład należy załatać odniesienie zaimportowane w pliku, w którym znajduje się testowana funkcja.

Załóżmy, że masz plik functions.py, w którym są następujące:

import datetime

def get_today():
    return datetime.date.today()

wtedy w swoim teście powinieneś mieć coś takiego

import datetime
import unittest

from functions import get_today
from mock import patch, Mock

class GetTodayTest(unittest.TestCase):

    @patch('functions.datetime')
    def test_get_today(self, datetime_mock):
        datetime_mock.date.today = Mock(return_value=datetime.strptime('Jun 1 2005', '%b %d %Y'))
        value = get_today()
        # then assert your thing...

Mam nadzieję, że to trochę pomoże.


Wygląda to bardzo atrakcyjnie, ale nie mogę tego uruchomić (rzuca NameError: name 'datetime' is not defined). Skąd pochodzi datetime.strptimeodwołanie, Mock(return_value=...)jeśli nie importujesz datetimepliku testowego? AKTUALIZACJA: W porządku, po prostu zaimportowałem datetimemoduł do pliku testowego. Pomyślałem, że sztuczka datetimepolega na tym, że ukrywasz odniesienie z pliku testowego.
imrek

@DrunkenMaster Musiałbym zobaczyć przykład tego, co robiłeś i które odniesienie kpiłeś. robiłeś import datetimeczy from datetime import strptime? gdybyś robił pierwszą z nich, musiałbyś kpić datetimei robiła mocked_datetime.strptime.return_value = whatever, to druga, musiałbyś bezpośrednio mockować referencję strptime w pliku, w którym znajduje się testowana metoda.
iferminm

@israelord Chciałem powiedzieć, że w ostatnim fragmencie kodu (pliku testowym) brakuje importu, aby odwołanie do daty i godziny Mock(return_value=datetime...)działało.
imrek

32

Aby dodać do rozwiązania Daniela G :

from datetime import date

class FakeDate(date):
    "A manipulable date replacement"
    def __new__(cls, *args, **kwargs):
        return date.__new__(date, *args, **kwargs)

Tworzy to klasę, która po utworzeniu zwróci normalny obiekt datetime.date, ale którą można również zmienić.

@mock.patch('datetime.date', FakeDate)
def test():
    from datetime import date
    FakeDate.today = classmethod(lambda cls: date(2010, 1, 1))
    return date.today()

test() # datetime.date(2010, 1, 1)

2
Bądź tutaj bardzo ostrożny - musisz użyć wersji from, w przeciwnym razie możesz uzyskać dziwne rzeczy, jeśli użyjesz datetime.date (lub datetime lub innych). IE - głębokość stosu osiągnięta, gdy twoje fałszywe nowe wywołania.
Danny Staple

Nie będziesz miał tego problemu, jeśli fałszywy obiekt znajduje się we własnym module: dpaste.com/790309 . Chociaż nawet jeśli znajduje się w tym samym module, co fałszywa funkcja, to nie importuje date/ datetimesama, używa zmiennej globalnie dostępnej, więc nie powinno być problemu: dpaste.com/790310
eternicode

mniej krótkie wyjaśnienie można znaleźć tutaj: williamjohnbert.com/2011/07/…
ezdazuzena

9

Kilka dni temu miałem taką samą sytuację, a moim rozwiązaniem było zdefiniowanie funkcji w module do testowania i po prostu mock:

def get_date_now():
    return datetime.datetime.now()

Dziś dowiedziałem się o FreezeGun i wydaje się, że pięknie pokrywa tę sprawę

from freezegun import freeze_time
import datetime
import unittest


@freeze_time("2012-01-14")
def test():
    assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)

9

Najłatwiej dla mnie zrobić to:

import datetime
from unittest.mock import Mock, patch

def test():
    datetime_mock = Mock(wraps=datetime.datetime)
    datetime_mock.now.return_value = datetime.datetime(1999, 1, 1)
    with patch('datetime.datetime', new=datetime_mock):
        assert datetime.datetime.now() == datetime.datetime(1999, 1, 1)

UWAGA dla tego rozwiązania: wszystkie funkcje od datetime moduleod target_modulepracy zostanie zatrzymane.


1
To jest naprawdę ładne i zwięzłe. Linię datetime_mock.now = Mock(return_value=datetime(1999, 1, 1)można było nawet skrócić do datetime_mock.now.return_value = datetime(1999, 1, 1). Zamiast rozpoczynać łatkę start()za pomocą, rozważ użycie with patch(...):menedżera kontekstu, aby upewnić się, że datetimepo zakończeniu testu zachowuje się normalnie (odblokowany).
Dirk

Zawsze wolę rozwiązanie wykorzystujące wbudowaną bibliotekę
Nam G VU

@ frx08 Czy mogę wiedzieć, jak zresetować to kpiny? Mam na myśli, jak datetime.datetime.now()odblokować ^^?
Nam G VU

Dobrze po próbuje użyć tej makiety - Jedna uwaga dla tego rozwiązania jest pełna funkcjonalność od datetime moduleod target_moduleprzestanie działać.
Nam G VU

1
Zgadzam się, @ frx08 with () zmniejszyłoby ból. Chociaż wewnątrz tego bloku wszystkie, np. Data, timedelta przestanie działać. Co jeśli teraz musimy wyszydzać, ale matematyka na randkach nadal działa? Przepraszamy, musimy mieć .now () wyszydzony tylko nie cały moduł datetime.
Nam G VU

7

Możesz zastosować następujące podejście, oparte na rozwiązaniu Daniela G. Ten ma tę zaletę, że nie przerywa sprawdzania typów isinstance(d, datetime.date).

import mock

def fixed_today(today):
    from datetime import date

    class FakeDateType(type):
        def __instancecheck__(self, instance):
            return isinstance(instance, date)

    class FakeDate(date):
        __metaclass__ = FakeDateType

        def __new__(cls, *args, **kwargs):
            return date.__new__(date, *args, **kwargs)

        @staticmethod
        def today():
            return today

    return mock.patch("datetime.date", FakeDate)

Zasadniczo zastępujemy datetime.dateklasę opartą na języku C naszą własną podklasą Pythona, która tworzy oryginalne datetime.dateinstancje i odpowiada na isinstance()zapytania dokładnie tak samo jak natywna datetime.date.

Użyj go jako menedżera kontekstu w swoich testach:

with fixed_today(datetime.date(2013, 11, 22)):
    # run the code under test
    # note, that these type checks will not break when patch is active:
    assert isinstance(datetime.date.today(), datetime.date)

Podobne podejście można zastosować do kpiny z datetime.datetime.now()funkcji.


Nie jestem pewien, czy to działa w Pythonie 2.7. Otrzymuję maksymalną głębokość rekurencji RuntimeError z __instancecheck__metodą.
Dan Loewenherz

To rzeczywiście działa w Pythonie 2.7 i rozwiązało mój problem ze sprawdzaniem typu instancji, dzięki!
Karatheodory

4

Ogólnie rzecz biorąc, musiałbyś datetimelub może datetime.dategdzieś zaimportować do modułu. Bardziej efektywnym sposobem na oszukanie metody byłoby załatanie jej na module, który ją importuje. Przykład:

a.py

from datetime import date

def my_method():
    return date.today()

Następnie na potrzeby testu sam obiekt pozorowany zostanie przekazany jako argument do metody testowej. Możesz skonfigurować makietę z żądaną wartością wyniku, a następnie wywołać testowaną metodę. Następnie zapewniłbyś, że twoja metoda zrobiła to, co chcesz.

>>> import mock
>>> import a
>>> @mock.patch('a.date')
... def test_my_method(date_mock):
...     date_mock.today.return_value = mock.sentinel.today
...     result = a.my_method()
...     print result
...     date_mock.today.assert_called_once_with()
...     assert mock.sentinel.today == result
...
>>> test_my_method()
sentinel.today

Słowo ostrzeżenia. Z kpiną z całą pewnością można przejść za burtę. Kiedy to zrobisz, twoje testy będą dłuższe, trudniejsze do zrozumienia i niemożliwe do utrzymania. Zanim zaczniesz kpić z tak prostej metody datetime.date.today, zadaj sobie pytanie, czy naprawdę musisz z niej kpić. Jeśli twój test jest krótki i rzeczowy i działa dobrze bez kpiny z funkcji, możesz po prostu patrzeć na wewnętrzny szczegół testowanego kodu, a nie obiekt, który chcesz wyszydzić.


2

Oto inny sposób na mockowanie datetime.date.today()z dodatkową premią polegającą na tym, że pozostałe datetimefunkcje nadal działają, ponieważ obiekt pozorowany jest skonfigurowany do pakowania oryginalnego datetimemodułu:

from unittest import mock, TestCase

import foo_module

class FooTest(TestCase):

    @mock.patch(f'{foo_module.__name__}.datetime', wraps=datetime)
    def test_something(self, mock_datetime):
        # mock only datetime.date.today()
        mock_datetime.date.today.return_value = datetime.date(2019, 3, 15)
        # other calls to datetime functions will be forwarded to original datetime

Zwróć uwagę na wraps=datetimeargument mock.patch()- gdy foo_moduleużywa innych datetimefunkcji oprócz date.today()tego, zostaną one przekazane do oryginalnego opakowanego datetimemodułu.


1
Świetna odpowiedź, większość testów, w których musisz udawać datę, musisz użyć modułu datetime
Antoine Vo

1

Kilka rozwiązań omówiono w http://blog.xelnor.net/python-mocking-datetime/ . W podsumowaniu:

Mock object - Prosty i skuteczny, ale przerywa sprawdzanie instancji ():

target = datetime.datetime(2009, 1, 1)
with mock.patch.object(datetime, 'datetime', mock.Mock(wraps=datetime.datetime)) as patched:
    patched.now.return_value = target
    print(datetime.datetime.now())

Mock klasa

import datetime
import mock

real_datetime_class = datetime.datetime

def mock_datetime_now(target, dt):
    class DatetimeSubclassMeta(type):
        @classmethod
        def __instancecheck__(mcs, obj):
            return isinstance(obj, real_datetime_class)

    class BaseMockedDatetime(real_datetime_class):
        @classmethod
        def now(cls, tz=None):
            return target.replace(tzinfo=tz)

        @classmethod
        def utcnow(cls):
            return target

    # Python2 & Python3 compatible metaclass
    MockedDatetime = DatetimeSubclassMeta('datetime', (BaseMockedDatetime,), {})

    return mock.patch.object(dt, 'datetime', MockedDatetime)

Użyj jako:

with mock_datetime_now(target, datetime):
   ....


0

Zaimplementowałem metodę @ user3016183 używając niestandardowego dekoratora:

def changeNow(func, newNow = datetime(2015, 11, 23, 12, 00, 00)):
    """decorator used to change datetime.datetime.now() in the tested function."""
    def retfunc(self):                             
        with mock.patch('mymodule.datetime') as mock_date:                         
            mock_date.now.return_value = newNow
            mock_date.side_effect = lambda *args, **kw: datetime(*args, **kw)
            func(self)
    return retfunc

Pomyślałem, że to może kiedyś komuś pomóc ...


0

Możliwe jest mockowanie funkcji z datetimemodułu bez dodawaniaside_effects

import mock
from datetime import datetime
from where_datetime_used import do

initial_date = datetime.strptime('2018-09-27', "%Y-%m-%d")
with mock.patch('where_datetime_used.datetime') as mocked_dt:
    mocked_dt.now.return_value = initial_date
    do()

0

Dla tych z Was korzystających z pytesta z mockerem tutaj jest jak kpiłem datetime.datetime.now()co jest bardzo podobne do oryginalnego pytania.

test_get_now(mocker):
    datetime_mock = mocker.patch("blackline_accounts_import.datetime",)
    datetime_mock.datetime.now.return_value=datetime.datetime(2019,3,11,6,2,0,0)

    now == function_being_tested()  # run function

    assert now == datetime.datetime(2019,3,11,6,2,0,0)

Zasadniczo makietę należy ustawić tak, aby zwracała określoną datę. Nie możesz bezpośrednio załatać obiektu datetime.


0

Wykonałem tę pracę, importując datetimejako realdatetimei zastępując metody, których potrzebowałem w makiecie, metodami rzeczywistymi:

import datetime as realdatetime

@mock.patch('datetime')
def test_method(self, mock_datetime):
    mock_datetime.today = realdatetime.today
    mock_datetime.now.return_value = realdatetime.datetime(2019, 8, 23, 14, 34, 8, 0)

0

Możesz kpić datetimeza pomocą tego:

W module sources.py:

import datetime


class ShowTime:
    def current_date():
        return datetime.date.today().strftime('%Y-%m-%d')

W twoim tests.py:

from unittest import TestCase, mock
import datetime


class TestShowTime(TestCase):
    def setUp(self) -> None:
        self.st = sources.ShowTime()
        super().setUp()

    @mock.patch('sources.datetime.date')
    def test_current_date(self, date_mock):
        date_mock.today.return_value = datetime.datetime(year=2019, month=10, day=1)
        current_date = self.st.current_date()
        self.assertEqual(current_date, '2019-10-01')

co jest sourcesw twoim dekoratorze łat?
elena

Droga @eleno, raczej ciężko sobie przypomnieć, o czym myślałem prawie rok temu)). Myślę, że chodziło mi o dowolny moduł z naszych źródeł aplikacji - tylko kod Twojej aplikacji.
MTMobile

0

CPython faktycznie implementuje moduł datetime, używając zarówno czystego języka Python Lib / datetime.py, jak i zoptymalizowanych pod kątem C modułów / _datetimemodule.c . Wersja zoptymalizowana pod kątem języka C nie może być łatana, ale wersja oparta na czystym Pythonie tak.

U dołu implementacji czystego Pythona w Lib / datetime.py znajduje się następujący kod:

try:
    from _datetime import *  # <-- Import from C-optimized module.
except ImportError:
    pass

Ten kod importuje wszystkie definicje zoptymalizowane pod kątem języka C i skutecznie zastępuje wszystkie definicje w czystym języku Python. Możemy zmusić CPythona do używania implementacji modułu datetime w czystym Pythonie, wykonując:

import datetime
import importlib
import sys

sys.modules["_datetime"] = None
importlib.reload(datetime)

Ustawiając sys.modules["_datetime"] = None, mówimy Pythonowi, aby ignorował moduł zoptymalizowany pod kątem C. Następnie przeładowujemy moduł, który powoduje import z_datetime niepowodzenie . Teraz definicje czystego Pythona pozostają i można je normalnie załatać.

Jeśli używasz Pytest, dołącz powyższy fragment kodu do conftest.py i możesz datetimenormalnie łatać obiekty.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.