Tentando simular datetime.date.today (), mas não está funcionando

158

Alguém pode me dizer por que isso não está funcionando?

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

Talvez alguém possa sugerir uma maneira melhor?

Belmin Fernandez
fonte
1
Documentos da mockbiblioteca: voidspace.org.uk/python/mock/examples.html#partial-mocking
guettli
2
freezegun
Ullauri

Respostas:

124

Há alguns problemas.

Primeiro de tudo, a maneira como você está usando mock.patchnão é muito correta. Quando usado como decorador, ele substitui a função / classe fornecida (neste caso datetime.date.today) por um Mockobjeto apenas dentro da função decorada . Portanto, somente dentro de você today()haverá datetime.date.todayuma função diferente, que não parece ser o que você deseja.

O que você realmente quer parece mais com isso:

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

Infelizmente, isso não vai funcionar:

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

Isso falha porque os tipos internos do Python são imutáveis ​​- consulte esta resposta para obter mais detalhes.

Nesse caso, eu subclasseia datetime.date e criaria a função correta:

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

E agora você pode fazer:

>>> datetime.date.today()
NewDate(2010, 1, 1)
Daniel G
fonte
13
uma boa solução, mas infelizmente causa problemas com a decapagem.
Baczek 22/03/11
14
Embora essa resposta seja boa, é possível simular data e hora sem criar uma classe: stackoverflow.com/a/25652721/117268
Emil Stenström
Como você restauraria a datetimeinstância para o valor original? com deepcoppy?
precisa saber é o seguinte
5
Muito mais fácil de fazer: #patch('mymodule.datetime', Mock(today=lambda: date(2017, 11, 29)))
Victor Gavro
1
Mais muito mais fácil de fazer @patch('module_you_want_to_test.date', Mock( today=Mock(return_value=datetime.date(2017, 11, 29)))).
Jonhy Beebop
162

Outra opção é usar https://github.com/spulec/freezegun/

Instale-o:

pip install freezegun

E use-o:

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

Também afeta outras chamadas de data e hora em chamadas de método de outros módulos:

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

E finalmente:

$ python main.py
# 2012-01-01
Mehdi Behrooz
fonte
13
Uma biblioteca muito, muito útil
Shaun
3
Você também pode tentar python-libfaketime se notar que seus testes de freezegun estão sendo executados lentamente.
Simon Weber
Ótima biblioteca, mas infelizmente não funciona bem com o NDB / Datastore do Google App Engine.
brandones
Eu amo que "freezegun" é o nome de uma biblioteca. Eu realmente amo desenvolvedores Python! :-D
MikeyE
Funciona, mas o freezegun parece lento, especialmente se você tiver uma lógica complicada com várias chamadas no horário atual.
precisa saber é o seguinte
115

Quanto vale a pena, os documentos do Mock falam sobre datetime.date.today especificamente, e é possível fazer isso sem a necessidade de criar uma classe fictícia:

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
fonte
2
Isso realmente não funcionou para mim. Embora eu aprecie o esforço em localizar a entrada.
Pradyot
8
o que significa "mymodule" na função patch?
Seufagner 02/09/2015
4
Encontrei o link aqui em "Zombaria Parcial"
Leo C Han
3
O mymodule do @seufagner é explicado de maneira bastante confusa em voidspace.org.uk/python/mock/patch.html#where-to-patch . Parece que se o seu módulo usa from datetime import dateentão é o nome do módulo onde from datetime import dateea chamada para date.today()aparecer
Danio
1
Obrigado. Trabalhou! Exemplo: com mock.patch ('tests.views.datetime') como mock_date: mock_date.today.return_value = datetime.datetime (2016, 9, 18) mock_date.side_effect = lambda * args, ** kw: date (* args , ** kw)
Latrova 28/09/16
36

Acho que cheguei um pouco tarde para isso, mas acho que o principal problema aqui é que você está corrigindo o datetime.date.today diretamente e, de acordo com a documentação, isso está errado.

Você deve corrigir a referência importada no arquivo em que a função testada está, por exemplo.

Digamos que você tenha um arquivo functions.py no qual você tenha o seguinte:

import datetime

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

então, no seu teste, você deve ter algo parecido com isto

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

Espero que isso ajude um pouco.

iferminm
fonte
Isso parece muito atraente, mas não consigo fazer isso funcionar (lança a NameError: name 'datetime' is not defined). De onde vem a datetime.strptimereferência Mock(return_value=...)se você não está importando datetimeno seu arquivo de teste? ATUALIZAÇÃO: Tudo bem, eu apenas fui em frente e importei o datetimemódulo no arquivo de teste. Eu pensei que o truque era como você está escondendo a datetimereferência do arquivo de teste.
Imrek
@DrunkenMaster Eu precisaria ver um exemplo do que você estava fazendo e de qual referência estava zombando. você estava fazendo import datetimeou from datetime import strptime? se você estivesse fazendo o primeiro, teria que zombar datetimee fazer mocked_datetime.strptime.return_value = whatever, é o posterior, teria que zombar diretamente da referência de strptime no arquivo em que o método testado mora.
Iferminm #
@israelord O que eu quis dizer é que seu último snippet de código (o arquivo de teste) está faltando uma importação para que a referência de data e hora Mock(return_value=datetime...)funcione.
Imrek
32

Para adicionar à solução 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)

Isso cria uma classe que, quando instanciada, retornará um objeto datetime.date normal, mas que também poderá ser alterado.

@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
fonte
2
Tenha muito cuidado aqui - você deve usar a versão from; caso contrário, poderá obter estranheza se usar datetime.date (ou datetime ou outros). IE - profundidade da pilha atingida quando suas novas chamadas falsas se chamam.
Danny Staple
Você não terá esse problema se o objeto falso estiver em seu próprio módulo: dpaste.com/790309 . Porém, mesmo que seja no mesmo módulo como a função ridicularizado, ele não importar date/ datetimesi mesma, ela usa a variável globalmente disponível, então não deve haver nenhum problema: dpaste.com/790310
eternicode
uma explicação menos breve pode ser encontrada aqui: williamjohnbert.com/2011/07/…
ezdazuzena
9

Eu enfrentei a mesma situação alguns dias atrás, e minha solução foi definir uma função no módulo para testar e apenas zombar disso:

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

Hoje eu descobri o FreezeGun e parece cobrir esse caso lindamente

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

A maneira mais fácil para mim é fazer isso:

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)

CUIDADO para esta solução: todas as funcionalidades da datetime modulepartir do target_moduleirão parar de funcionar.

frx08
fonte
1
Isso é realmente agradável e conciso. A linha datetime_mock.now = Mock(return_value=datetime(1999, 1, 1)pode até ser reduzida para datetime_mock.now.return_value = datetime(1999, 1, 1). Em vez de iniciar o patch start(), considere usar o with patch(...):gerenciador de contexto para garantir que ele datetimese comporte regularmente (desmembrado) novamente quando o teste terminar.
Dirk
Favorecer sempre de solução que fazer uso do built-in biblioteca
Nam G VU
@ frx08 Posso saber como redefinir essa zombaria? Quero dizer como ficar datetime.datetime.now()desmotivado ^^?
Nam G VU
Bem depois de tentar usar esta falsa - Um cuidado para esta solução é toda a funcionalidade a partir datetime moduledo target_modulevai parar de trabalhar.
Nam G VU
1
Concordo @ frx08 com with () subtil a dor. Embora dentro desse bloco, por exemplo, data, o timedelta pare de funcionar. E se precisarmos agora zombado, mas a matemática da data ainda continuar? Desculpe, devemos ter .now () zombado apenas não todo o módulo datetime.
Nam G VU
7

Você pode usar a seguinte abordagem, com base na solução de Daniel G. Este tem a vantagem de não quebrar a verificação de tipo 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)

Basicamente, substituímos a datetime.dateclasse baseada em C por nossa própria subclasse python, que produz datetime.dateinstâncias originais e responde a isinstance()consultas exatamente como nativasdatetime.date .

Use-o como gerenciador de contexto em seus testes:

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)

Abordagem semelhante pode ser usada para zombar da datetime.datetime.now()função.

Andrey Lebedev
fonte
Não tenho certeza se isso funciona no Python 2.7. Estou recebendo um RuntimeError com profundidade de recursão máxima com o __instancecheck__método
Dan Loewenherz
Isso realmente funciona no Python 2.7 e resolveu meu problema com a verificação do tipo de instância, obrigado!
22817 Karatheodory
4

De um modo geral, você teria datetimeou talvezdatetime.date importado para um módulo em algum lugar. Uma maneira mais eficaz de zombar do método seria corrigi-lo no módulo que o está importando. Exemplo:

a.py

from datetime import date

def my_method():
    return date.today()

Em seguida, para o seu teste, o próprio objeto simulado seria passado como argumento para o método de teste. Você configuraria a simulação com o valor do resultado desejado e, em seguida, chamaria seu método em teste. Então você afirma que seu método fez o que deseja.

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

Uma palavra de alerta. É certamente possível exagerar na zombaria. Quando você o faz, torna seus testes mais longos, mais difíceis de entender e impossíveis de manter. Antes de zombar de um método tão simples quanto datetime.date.today, pergunte a si mesmo se você realmente precisa zombar dele. Se o seu teste for curto e direto e funcionar bem sem zombar da função, você pode estar apenas olhando para um detalhe interno do código que está testando, em vez de um objeto que você precisa zombar.

jpmc26
fonte
2

Aqui está outra maneira de zombar, datetime.date.today()com um bônus adicional, de que o restante das datetimefunções continua funcionando, pois o objeto de zombaria está configurado para envolver o datetimemódulo original :

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

Observe o wraps=datetimeargumento para mock.patch()- quando ele foo_moduleusar outras datetimefunções além delas date.today(), serão encaminhadas para o datetimemódulo empacotado original .

Mrts
fonte
1
Grande resposta, a maioria dos testes em que você precisa para zombar data que você vai precisar usar datetime módulo
Antoine Vo
1

Várias soluções são discutidas em http://blog.xelnor.net/python-mocking-datetime/ . Em suma:

Objeto simulado - Simples e eficiente, mas quebra as verificações 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())

Mock class

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)

Use como:

with mock_datetime_now(target, datetime):
   ....
eddygeek
fonte
0

Eu implementei o método @ user3016183 usando um decorador personalizado:

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

Eu pensei que isso poderia ajudar alguém um dia ...

DainDwarf
fonte
0

É possível simular funções do datetimemódulo sem adicionarside_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
fonte
0

Para aqueles que usam pytest com mocker, aqui está como eu zombei, o datetime.datetime.now()que é muito semelhante à pergunta original.

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)

Essencialmente, a simulação deve ser configurada para retornar a data especificada. Você não pode corrigir o objeto do datetime diretamente.

Daniel Butler
fonte
0

Eu fiz este trabalho, importando datetimecomo realdatetimee substituindo os métodos que eu precisava na simulação com os métodos reais:

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

Você pode zombar datetimeusando isso:

No módulo sources.py:

import datetime


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

No seu 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
fonte
o que há sourcesno seu decorador de patches?
elena
Caro @elena, é bastante difícil lembrar o que eu estava pensando há quase um ano)). Acho que quis dizer apenas qualquer módulo de nossas fontes de aplicativos - apenas o código do seu aplicativo.
MTMobile 04/07
0

CPython realmente implementa o módulo de data e hora usando tanto um puro-Python Lib / datetime.py e um optimizado-C módulos / _datetimemodule.c . A versão otimizada para C não pode ser corrigida, mas a versão pura em Python pode.

Na parte inferior da implementação do Python puro em Lib / datetime.py está este código:

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

Esse código importa todas as definições otimizadas para C e substitui efetivamente todas as definições de Python puro. Podemos forçar o CPython a usar a implementação em Python puro do módulo datetime, fazendo:

import datetime
import importlib
import sys

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

Ao definir sys.modules["_datetime"] = None, dizemos ao Python para ignorar o módulo otimizado para C. Em seguida, recarregamos o módulo que causa a importação de_datetime falha . Agora, as definições de Python puro permanecem e podem ser corrigidas normalmente.

Se você estiver usando o Pytest , inclua o trecho acima em conftest.py e poderá corrigir os datetimeobjetos normalmente.

GrantJ
fonte