Essayer de simuler datetime.date.today (), mais ne fonctionne pas

158

Quelqu'un peut-il me dire pourquoi cela ne fonctionne pas?

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

Peut-être que quelqu'un pourrait suggérer une meilleure solution?

Belmin Fernandez
la source
1
Docs de la mockbibliothèque: voidspace.org.uk/python/mock/examples.html#partial-mocking
guettli
2
freezegun
Ullauri

Réponses:

125

Il y a quelques problèmes.

Tout d'abord, la façon dont vous utilisez mock.patchn'est pas tout à fait correcte. Lorsqu'il est utilisé comme décorateur, il remplace la fonction / classe donnée (dans ce cas, datetime.date.today) par un Mockobjet uniquement dans la fonction décorée . Donc, seulement dans votre today()volonté il datetime.date.todayy aura une fonction différente, qui ne semble pas être ce que vous voulez.

Ce que vous voulez vraiment ressemble plus à ceci:

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

Malheureusement, cela ne fonctionnera pas:

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

Cela échoue car les types intégrés Python sont immuables - voir cette réponse pour plus de détails.

Dans ce cas, je sous-classerais moi-même datetime.date et créerais la bonne fonction:

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

Et maintenant vous pouvez faire:

>>> datetime.date.today()
NewDate(2010, 1, 1)
Daniel G
la source
13
une belle solution, mais qui pose malheureusement des problèmes de décapage.
Baczek
14
Bien que cette réponse soit bonne, il est possible de simuler datetime sans créer de classe: stackoverflow.com/a/25652721/117268
Emil Stenström
Comment restaureriez-vous l' datetimeinstance à sa valeur d'origine? avec deepcoppy?
Oleg Belousov
5
Beaucoup plus facile à faire:patch('mymodule.datetime', Mock(today=lambda: date(2017, 11, 29)))
Victor Gavro
1
Plus facile à faire @patch('module_you_want_to_test.date', Mock( today=Mock(return_value=datetime.date(2017, 11, 29)))).
Jonhy Beebop
163

Une autre option consiste à utiliser https://github.com/spulec/freezegun/

Installez-le:

pip install freezegun

Et utilisez-le:

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

Cela affecte également les autres appels datetime dans les appels de méthode à partir d'autres modules:

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

Et enfin:

$ python main.py
# 2012-01-01
Mehdi Behrooz
la source
13
Une bibliothèque très très utile
Shaun
3
Vous pouvez également essayer python-libfaketime si vous remarquez que vos tests de congélation fonctionnent lentement.
Simon Weber
Excellente bibliothèque, mais ne fonctionne malheureusement pas bien avec Google App Engine NDB / Datastore.
brandones
J'adore que "freezegun" soit le nom d'une bibliothèque. J'adore vraiment les développeurs Python! :-D
MikeyE
Fonctionne, mais le gel semble lent, surtout si vous avez une logique compliquée avec plusieurs appels pour l'heure actuelle.
Andrey Belyak
115

Pour ce que ça vaut, les documents Mock parlent spécifiquement de datetime.date.today, et il est possible de le faire sans avoir à créer une classe factice:

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
la source
2
Cela n'a pas vraiment fonctionné pour moi. Bien que j'apprécie les efforts déployés pour localiser l'entrée.
Pradyot
8
que signifie "mon module" à la fonction patch?
seufagner le
4
Trouvé le lien ici sous "Mocking partiel"
Leo C Han
3
@seufagner mymodule est expliqué d'une manière assez déroutante sur voidspace.org.uk/python/mock/patch.html#where-to-patch . Il semble que si votre module utilise from datetime import datealors c'est le nom du module où from datetime import dateet l'appel à date.today()apparaît
danio
1
Merci. Travaillé! Exemple: avec mock.patch ('tests.views.datetime') comme mock_date: mock_date.today.return_value = datetime.datetime (2016, 9, 18) mock_date.side_effect = lambda * args, ** kw: date (* args , ** kw)
Latrova
36

Je suppose que je suis arrivé un peu en retard pour cela, mais je pense que le problème principal ici est que vous corrigez directement datetime.date.today et, selon la documentation, c'est faux.

Vous devez patcher la référence importée dans le fichier où se trouve la fonction testée, par exemple.

Supposons que vous ayez un fichier functions.py contenant les éléments suivants:

import datetime

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

alors, dans votre test, vous devriez avoir quelque chose comme ça

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

J'espère que cela aide un peu.

iferminm
la source
Cela semble très convaincant, mais je ne peux pas le faire fonctionner (lance un NameError: name 'datetime' is not defined). D'où vient la datetime.strptimeréférence Mock(return_value=...)si vous n'importez pas datetimedans votre fichier de test? MISE À JOUR: C'est OK, j'ai juste continué et j'ai importé le datetimemodule dans le fichier de test. Je pensais que le truc était de savoir comment cacher la datetimeréférence du fichier de test.
imrek
@DrunkenMaster Je devrais voir un exemple de ce que vous faisiez et de quelle référence vous vous moquiez. faisiez-vous import datetimeou from datetime import strptime? si vous faisiez le premier, vous auriez à vous moquer datetimeet à faire mocked_datetime.strptime.return_value = whatever, est le plus tard, vous devriez vous moquer directement de la référence strptime dans le fichier où réside la méthode testée.
iferminm
@israelord Ce que je voulais dire, c'est que votre dernier extrait de code (le fichier de test) manque une importation pour que la référence datetime Mock(return_value=datetime...)fonctionne.
imrek
32

Pour ajouter à la solution de Daniel G :

from datetime import date

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

Cela crée une classe qui, une fois instanciée, retournera un objet datetime.date normal, mais qui peut également être modifiée.

@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)
éternicode
la source
2
Soyez très prudent ici - vous devez utiliser la version from, sinon vous risquez d'avoir des bizarreries si vous utilisez datetime.date (ou datetime ou autres). IE - profondeur de pile atteinte lorsque vos faux nouveaux appels lui-même.
Danny Staple
Vous n'aurez pas ce problème si le faux objet est dans son propre module: dpaste.com/790309 . Bien que, même s'il se trouve dans le même module que la fonction simulée, il ne s'importe pas date/ datetimelui - même, il utilise la variable disponible globalement, il ne devrait donc pas y avoir de problème: dpaste.com/790310
eternicode
une explication moins brève peut être trouvée ici: williamjohnbert.com/2011/07/…
ezdazuzena
9

J'ai fait face à la même situation il y a quelques jours, et ma solution était de définir une fonction dans le module pour tester et simplement se moquer de cela:

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

Aujourd'hui, j'ai découvert FreezeGun , et il semble couvrir magnifiquement ce cas

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
la source
9

Le moyen le plus simple pour moi est de faire ceci:

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)

ATTENTION pour cette solution: toutes les fonctionnalités de datetime modulede target_modulecesseront de fonctionner.

frx08
la source
1
C'est vraiment sympa et concis. La ligne datetime_mock.now = Mock(return_value=datetime(1999, 1, 1)pourrait même être raccourcie datetime_mock.now.return_value = datetime(1999, 1, 1). Au lieu de démarrer le correctif avec start(), envisagez d'utiliser le with patch(...):gestionnaire de contexte pour vous assurer qu'il datetimese comporte à nouveau régulièrement (non simulé) à la fin de votre test.
Dirk
Toujours en faveur d'une solution qui utilise la bibliothèque intégrée
Nam G VU
@ frx08 Puis-je savoir comment réinitialiser cette moquerie? Je veux dire comment se datetime.datetime.now()défaire ^^?
Nam G VU
Eh bien après avoir essayé d'utiliser cette maquette - une mise en garde pour cette solution est de toutes les fonctionnalités datetime moduledu target_modulecessera de fonctionner.
Nam G VU
1
D'accord @ frx08 le with () atténuerait la douleur. Bien qu'à l'intérieur de ce bloc, par exemple la date, timedelta cessera de fonctionner. Et si nous avons besoin de se moquer maintenant mais que les calculs de date continuent? Désolé, nous devons avoir .now () se moquent de tout le module datetime.
Nam G VU
7

Vous pouvez utiliser l'approche suivante, basée sur la solution Daniel G. Celui-ci a l'avantage de ne pas casser la vérification de type avec 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)

Fondamentalement, nous remplaçons la datetime.dateclasse basée sur C par notre propre sous-classe python, qui produit des datetime.dateinstances originales et répond aux isinstance()requêtes exactement comme natif datetime.date.

Utilisez-le comme gestionnaire de contexte dans vos 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)

Une approche similaire peut être utilisée pour simuler la datetime.datetime.now()fonction.

Andrey Lebedev
la source
Je ne suis pas sûr que cela fonctionne dans Python 2.7. J'obtiens une profondeur de récursivité maximale RuntimeError avec la __instancecheck__méthode.
Dan Loewenherz
Cela fonctionne en effet dans Python 2.7, et cela a résolu mon problème avec la vérification du type d'instance, merci!
Karatheodory le
4

De manière générale, vous auriez datetimeou peut-être datetime.dateimporté dans un module quelque part. Un moyen plus efficace de se moquer de la méthode serait de la patcher sur le module qui l'importe. Exemple:

a.py

from datetime import date

def my_method():
    return date.today()

Ensuite, pour votre test, l'objet fictif lui-même serait passé en argument à la méthode de test. Vous devez configurer la maquette avec la valeur de résultat souhaitée, puis appeler votre méthode sous test. Ensuite, vous affirmeriez que votre méthode a fait ce que vous voulez.

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

Un mot d'avertissement. Il est très certainement possible d’exagérer en se moquant. Lorsque vous le faites, vos tests sont plus longs, plus difficiles à comprendre et impossibles à maintenir. Avant de vous moquer d'une méthode aussi simple que cela datetime.date.today, demandez-vous si vous avez vraiment besoin de vous en moquer. Si votre test est court et précis et fonctionne correctement sans se moquer de la fonction, vous pouvez simplement regarder un détail interne du code que vous testez plutôt qu'un objet dont vous devez vous moquer.

jpmc26
la source
2

Voici une autre façon de se moquer datetime.date.today()avec un bonus supplémentaire du fait que le reste des datetimefonctions continue de fonctionner, car l'objet fictif est configuré pour envelopper le datetimemodule d' origine :

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

Notez l' wraps=datetimeargument à mock.patch()- lorsque le foo_moduleutilise d'autres datetimefonctions en plus, date.today()elles seront transmises au datetimemodule encapsulé d'origine .

mrts
la source
1
Excellente réponse, la plupart des tests où vous devez simuler la date, vous devrez utiliser le module datetime
Antoine Vo
1

Plusieurs solutions sont décrites dans http://blog.xelnor.net/python-mocking-datetime/ . En résumé:

Objet simulé - Simple et efficace mais interrompt les vérifications isinstance ():

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

Classe simulée

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)

Utilisé comme:

with mock_datetime_now(target, datetime):
   ....
Eddygeek
la source
0

J'ai implémenté la méthode @ user3016183 en utilisant un décorateur personnalisé:

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

J'ai pensé que ça pourrait aider quelqu'un un jour ...

Nain
la source
0

Il est possible de simuler des fonctions du datetimemodule sans ajouterside_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
la source
0

Pour ceux d'entre vous qui utilisent pytest avec moqueur, voici comment je me suis moqué, datetime.datetime.now()ce qui est très similaire à la question initiale.

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)

Essentiellement, la maquette doit être définie pour renvoyer la date spécifiée. Vous ne pouvez pas patcher directement sur l'objet de datetime.

Daniel Butler
la source
0

J'ai fait ce travail en important datetimecomme realdatetimeet remplacer les méthodes dont je avais besoin dans la maquette avec les méthodes réelles:

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
la source
0

Vous pouvez vous moquer en datetimeutilisant ceci:

Dans le module sources.py:

import datetime


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

Dans votre 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
la source
qu'y a-t-il sourcesdans votre décorateur de patchs?
elena
Chère @elena, il est assez difficile de me souvenir de ce à quoi je pensais il y a presque un an)). Je suppose que je voulais dire n'importe quel module de nos sources d'application - juste le code de votre application.
MTMobile
0

CPython implémente en fait le module datetime en utilisant à la fois un lib / datetime.py pur-Python et un module optimisé C / _datetimemodule.c . La version optimisée C ne peut pas être corrigée, mais la version pure-Python le peut.

Au bas de l'implémentation pure-Python dans Lib / datetime.py se trouve ce code:

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

Ce code importe toutes les définitions optimisées C et remplace efficacement toutes les définitions pur-Python. Nous pouvons forcer CPython à utiliser l'implémentation pure-Python du module datetime en faisant:

import datetime
import importlib
import sys

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

En définissant sys.modules["_datetime"] = None, nous disons à Python d'ignorer le module optimisé C. Puis on recharge le module qui provoque l'importation depuis_datetime échec de . Maintenant, les définitions pur-Python restent et peuvent être corrigées normalement.

Si vous utilisez Pytest, incluez l'extrait ci-dessus dans conftest.py et vous pourrez patcher des datetimeobjets normalement.

GrantJ
la source