Der Versuch, datetime.date.today () zu verspotten, funktioniert jedoch nicht

158

Kann mir jemand sagen, warum das nicht funktioniert?

>>> 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)

Vielleicht könnte jemand einen besseren Weg vorschlagen?

Belmin Fernandez
quelle

Antworten:

124

Es gibt einige Probleme.

Erstens ist die Art und Weise, wie Sie sie verwenden, mock.patchnicht ganz richtig. Bei Verwendung als Dekorateur wird die angegebene Funktion / Klasse (in diesem Fall datetime.date.today) nur innerhalb der dekorierten Funktion durch ein MockObjekt ersetzt . Also nur in deinem Willentoday()datetime.date.today wird eine andere Funktion sein, die nicht das zu sein scheint, was Sie wollen.

Was Sie wirklich wollen, scheint eher so zu sein:

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

Leider funktioniert das nicht:

>>> 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'

Dies schlägt fehl, da die in Python integrierten Typen unveränderlich sind - siehe diese Antwort Weitere Informationen .

In diesem Fall würde ich datetime.date selbst unterordnen und die richtige Funktion erstellen:

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

Und jetzt könnten Sie tun:

>>> datetime.date.today()
NewDate(2010, 1, 1)
Daniel G.
quelle
13
eine schöne lösung, verursacht aber leider probleme beim beizen.
Baczek
14
Obwohl diese Antwort gut ist, ist es möglich, datetime zu verspotten, ohne eine Klasse zu erstellen: stackoverflow.com/a/25652721/117268
Emil Stenström
Wie würden Sie den datetimeursprünglichen Wert der Instanz wiederherstellen ? mit deepcoppy?
Oleg Belousov
5
Viel einfacher zu tun:patch('mymodule.datetime', Mock(today=lambda: date(2017, 11, 29)))
Victor Gavro
1
Viel einfacher zu machen @patch('module_you_want_to_test.date', Mock( today=Mock(return_value=datetime.date(2017, 11, 29)))).
Jonhy Beebop
162

Eine weitere Option ist die Verwendung von https://github.com/spulec/freezegun/.

Es installieren:

pip install freezegun

Und benutze es:

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

Dies wirkt sich auch auf andere datetime-Aufrufe in Methodenaufrufen von anderen Modulen aus:

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()

Und schlussendlich:

$ python main.py
# 2012-01-01
Mehdi Behrooz
quelle
13
Eine sehr sehr nützliche Bibliothek
Shaun
3
Sie können auch python-libfaketime ausprobieren, wenn Sie feststellen, dass Ihre Freezegun-Tests langsam ablaufen.
Simon Weber
Tolle Bibliothek, aber leider nicht gut mit Google App Engine NDB / Datastore.
Brandones
Ich liebe es, dass "freezegun" der Name einer Bibliothek ist. Ich liebe Python-Entwickler wirklich! :-D
MikeyE
Funktioniert, aber Freezegun scheint langsam zu sein, insbesondere wenn Sie eine komplizierte Logik mit mehreren Aufrufen für die aktuelle Zeit haben.
Andrey Belyak
115

Für das, was es wert ist, sprechen die Mock-Dokumente speziell über datetime.date.today, und dies ist möglich, ohne eine Dummy-Klasse erstellen zu müssen:

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)
...
kpup
quelle
2
Das hat bei mir nicht wirklich funktioniert. Ich schätze jedoch die Mühe, den Eintrag zu finden.
Pradyot
8
Was bedeutet "Mymodul" bei der Patch-Funktion?
Seufagner
4
Fand den Link hier unter "Partial Mocking"
Leo C Han
3
@seufagner mymodule wird unter voidspace.org.uk/python/mock/patch.html#where-to-patch ziemlich verwirrend erklärt . Es scheint, dass, wenn Ihr Modul verwendet, from datetime import datees der Name des Moduls ist, in dem from datetime import dateund der Aufruf zu date.today()erscheint
danio
1
Vielen Dank. Hat funktioniert! Beispiel: mit mock.patch ('tests.views.datetime') als mock_date: mock_date.today.return_value = datetime.datetime (2016, 9, 18) mock_date.side_effect = lambda * args, ** kw: date (* args , ** kw)
Latrova
36

Ich glaube, ich bin etwas spät gekommen, aber ich denke, das Hauptproblem hier ist, dass Sie datetime.date.today direkt patchen und dies laut Dokumentation falsch ist.

Sie sollten beispielsweise die Referenz patchen, die in die Datei importiert wurde, in der sich die getestete Funktion befindet.

Angenommen, Sie haben eine Datei functions.py, in der Sie Folgendes haben:

import datetime

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

dann sollten Sie in Ihrem Test so etwas haben

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...

Hoffe das hilft ein bisschen.

iferminm
quelle
Das sieht sehr überzeugend aus, aber ich kann das nicht zum Laufen bringen (wirft a NameError: name 'datetime' is not defined). Woher kommt die datetime.strptimeReferenz in Mock(return_value=...), wenn Sie nicht datetimein Ihre Testdatei importieren ? UPDATE: Es ist in Ordnung, ich habe gerade das datetimeModul in die Testdatei importiert . Ich dachte, der Trick war, wie Sie die datetimeReferenz aus der Testdatei verstecken .
Imrek
@ DrunkenMaster Ich müsste ein Beispiel dafür sehen, was Sie getan haben und welche Referenz Sie verspottet haben. hast du gemacht import datetimeoder from datetime import strptime? Wenn Sie die erste datetimemachen würden mocked_datetime.strptime.return_value = whatever, müssten Sie verspotten und tun , die spätere, Sie müssten die strptime-Referenz in der Datei, in der sich die getestete Methode befindet, direkt verspotten.
iferminm
@israelord Was ich damit sagen wollte, ist, dass Ihrem letzten Code-Snippet (der Testdatei) ein Import fehlt, damit die Datums- / Uhrzeitreferenz Mock(return_value=datetime...)funktioniert.
Imrek
32

So fügen Sie Daniel Gs Lösung hinzu :

from datetime import date

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

Dadurch wird eine Klasse erstellt, die bei Instanziierung ein normales datetime.date-Objekt zurückgibt, aber auch geändert werden kann.

@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)
Eternicode
quelle
2
Seien Sie hier sehr vorsichtig - Sie müssen die from-Version verwenden, da Sie sonst möglicherweise verrückt werden, wenn Sie datetime.date (oder datetime oder andere) verwenden. IE - Stapeltiefe erreicht, wenn Ihre gefälschten neuen Anrufe selbst.
Danny Staple
Dieses Problem tritt nicht auf, wenn sich das gefälschte Objekt in einem eigenen Modul befindet: dpaste.com/790309 . Auch wenn es sich im selben Modul wie die verspottete Funktion befindet, importiert es nicht date/ datetimeselbst, sondern verwendet die global verfügbare Variable, sodass es kein Problem geben sollte: dpaste.com/790310
eternicode
Eine weniger kurze Erklärung finden Sie hier: williamjohnbert.com/2011/07/…
ezdazuzena
9

Ich war vor ein paar Tagen mit der gleichen Situation konfrontiert, und meine Lösung bestand darin, eine Funktion im Modul zu definieren, um dies zu testen und einfach zu verspotten:

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

Heute habe ich von FreezeGun erfahren und es scheint diesen Fall wunderschön abzudecken

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)
Hito_kun
quelle
9

Der einfachste Weg für mich ist dies:

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)

VORSICHT für diese Lösung: Alle Funktionen von datetime modulevon target_modulefunktionieren nicht mehr.

frx08
quelle
1
Das ist wirklich schön und prägnant. Die Linie datetime_mock.now = Mock(return_value=datetime(1999, 1, 1)könnte sogar auf gekürzt werden datetime_mock.now.return_value = datetime(1999, 1, 1). Anstatt den Patch mit zu starten start(), sollten Sie den with patch(...):Kontextmanager verwenden, um sicherzustellen, dass er datetimesich nach Beendigung Ihres Tests wieder normal (nicht verspottet) verhält.
Dirk
Bevorzugen Sie immer eine Lösung, die die integrierte Bibliothek nutzt
Nam G VU
@ frx08 Darf ich wissen, wie ich diese Verspottung zurücksetzen kann? Ich meine, wie man sich datetime.datetime.now()verspottet ^^?
Nam G VU
Nun, nachdem Sie versucht haben, dieses Modell zu verwenden - eine VORSICHT für diese Lösung ist, dass alle Funktionen von datetime moduleab target_modulefunktionieren.
Nam G VU
1
Stimmen Sie @ frx08 zu, dass with () den Schmerz subtil machen würde. Obwohl innerhalb dieses Blocks alle z. B. Datum, funktioniert timedelta nicht mehr. Was ist, wenn wir uns jetzt verspotten müssen, aber die Datumsberechnung noch weitergeht? Entschuldigung, wir müssen .now () nur nicht das gesamte datetime-Modul verspottet haben.
Nam G VU
7

Sie können den folgenden Ansatz verwenden, der auf der Daniel G-Lösung basiert. Dieser hat den Vorteil, dass die Typprüfung nicht unterbrochen wird 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)

Grundsätzlich ersetzen wir die C-basierte datetime.dateKlasse durch unsere eigene Python-Unterklasse, die Originalinstanzen erzeugt datetime.dateund auf isinstance()Anfragen genau wie native antwortet datetime.date.

Verwenden Sie es als Kontextmanager in Ihren Tests:

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)

Ein ähnlicher Ansatz kann verwendet werden, um die datetime.datetime.now()Funktion zu verspotten .

Andrey Lebedev
quelle
Ich bin nicht sicher, ob dies in Python 2.7 funktioniert. Ich erhalte mit der __instancecheck__Methode eine maximale Rekursionstiefe RuntimeError .
Dan Loewenherz
Dies funktioniert in der Tat in Python 2.7 und hat mein Problem mit der Überprüfung des Instanztyps gelöst, danke!
Karatheodory
4

Im Allgemeinen hätten Sie datetimeoder vielleichtdatetime.date in ein Modul irgendwo importiert. Eine effektivere Möglichkeit, die Methode zu verspotten, besteht darin, sie auf dem Modul zu patchen, das sie importiert. Beispiel:

a.py.

from datetime import date

def my_method():
    return date.today()

Dann wird für Ihren Test das Scheinobjekt selbst als Argument an die Testmethode übergeben. Sie würden das Modell mit dem gewünschten Ergebniswert einrichten und dann Ihre zu testende Methode aufrufen. Dann würden Sie behaupten, dass Ihre Methode getan hat, was Sie wollen.

>>> 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

Ein Wort der Warnung. Es ist mit Sicherheit möglich, mit Spott über Bord zu gehen. Wenn Sie dies tun, werden Ihre Tests länger, schwerer zu verstehen und unmöglich zu warten. Bevor Sie eine Methode so einfach verspotten wie datetime.date.today, fragen Sie sich , wenn Sie wirklich brauchen , um es zu verspotten. Wenn Ihr Test kurz und präzise ist und ohne Verspottung der Funktion einwandfrei funktioniert, sehen Sie sich möglicherweise nur ein internes Detail des zu testenden Codes an und nicht ein Objekt, das Sie verspotten müssen.

jpmc26
quelle
2

Hier ist eine andere Möglichkeit, datetime.date.today()mit einem zusätzlichen Bonus zu verspotten, dass die restlichen datetimeFunktionen weiterhin funktionieren, da das Scheinobjekt so konfiguriert ist, dass es das ursprüngliche datetimeModul umschließt:

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

Beachten Sie das wraps=datetimeArgument: mock.patch()- Wenn foo_moduleandere datetimeFunktionen verwendet werden date.today(), werden diese an das ursprünglich umschlossene datetimeModul weitergeleitet.

mrts
quelle
1
Tolle Antwort, bei den meisten Tests, bei denen Sie das Datum verspotten müssen, müssen Sie das Datum / Uhrzeit-Modul verwenden
Antoine Vo
1

Verschiedene Lösungen werden unter http://blog.xelnor.net/python-mocking-datetime/ erläutert. . Zusammenfassend:

Mock - Objekt - Einfach und effizient , aber Pausen isinstance () prüft:

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())

Scheinklasse

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)

Benutzen als:

with mock_datetime_now(target, datetime):
   ....
Eddygeek
quelle
0

Ich habe die @ user3016183-Methode mit einem benutzerdefinierten Dekorator implementiert:

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

Ich dachte, das könnte eines Tages jemandem helfen ...

DainDwarf
quelle
0

Es ist möglich, Funktionen aus dem datetimeModul zu verspotten, ohne sie hinzuzufügenside_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()
Daniil Mashkin
quelle
0

Für diejenigen unter Ihnen, die Pytest mit Spötter verwenden, ist hier, wie ich verspottet habe, datetime.datetime.now()was der ursprünglichen Frage sehr ähnlich ist.

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)

Im Wesentlichen muss der Mock eingestellt werden, um das angegebene Datum zurückzugeben. Sie können das Objekt von datetime nicht direkt patchen.

Daniel Butler
quelle
0

Ich habe diese Arbeit durch den Import datetimeals realdatetimeund Ersetzen der Methoden , die ich mit den realen Methoden in der Mock benötigt:

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)
Adam McKenna
quelle
0

Sie können dies verspotten datetime:

Im Modul sources.py:

import datetime


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

In Ihrem 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')
MTMobile
quelle
was ist sources in Ihrem Patch-Dekorateur?
Elena
Liebe @elena, es ist ziemlich schwer sich zu erinnern, woran ich vor fast einem Jahr gedacht habe)). Ich denke, ich meinte nur ein beliebiges Modul unserer App-Quellen - nur den Code Ihrer Anwendung.
MTMobile
0

CPython implementiert das datetime-Modul tatsächlich mit einer reinen Python- Bibliothek / datetime.py und einem C-optimierten Modul / _datetimemodule.c . Die C-optimierte Version kann nicht gepatcht werden, die reine Python-Version jedoch.

Am Ende der Pure-Python-Implementierung in Lib / datetime.py befindet sich folgender Code:

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

Dieser Code importiert alle C-optimierten Definitionen und ersetzt effektiv alle reinen Python-Definitionen. Wir können CPython zwingen, die reine Python-Implementierung des datetime-Moduls zu verwenden, indem wir Folgendes tun:

import datetime
import importlib
import sys

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

Durch die Einstellung sys.modules["_datetime"] = Noneweisen wir Python an, das C-optimierte Modul zu ignorieren. Dann laden wir das Modul neu, das den Import von verursacht_datetime fehlschlägt. Jetzt bleiben die reinen Python-Definitionen erhalten und können normal gepatcht werden.

Wenn Sie Pytest verwenden Snippet in conftest.py ein, und Sie können datetimeObjekte normal patchen .

GrantJ
quelle