Mencoba untuk mengejek datetime.date.today (), tetapi tidak berfungsi

158

Adakah yang bisa memberi tahu saya mengapa ini tidak berhasil?

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

Mungkin seseorang bisa menyarankan cara yang lebih baik?

Belmin Fernandez
sumber
1
Documents mockperpustakaan: voidspace.org.uk/python/mock/examples.html#partial-mocking
guettli
2
freezegun
Ullauri

Jawaban:

125

Ada beberapa masalah.

Pertama-tama, cara Anda menggunakan mock.patchtidak sepenuhnya benar. Ketika digunakan sebagai dekorator, ia menggantikan fungsi / kelas yang diberikan (dalam hal ini, datetime.date.today) dengan Mockobjek hanya dalam fungsi yang didekorasi . Jadi, hanya di dalam Anda today()akan datetime.date.todayfungsi yang berbeda, yang tampaknya tidak seperti yang Anda inginkan.

Apa yang sebenarnya Anda inginkan tampaknya lebih seperti ini:

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

Sayangnya, ini tidak akan berhasil:

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

Ini gagal karena tipe bawaan Python tidak dapat diubah - lihat jawaban ini untuk lebih jelasnya.

Dalam hal ini, saya akan membuat subclass datetime.date sendiri dan membuat fungsi yang tepat:

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

Dan sekarang Anda bisa melakukannya:

>>> datetime.date.today()
NewDate(2010, 1, 1)
Daniel G
sumber
13
solusi yang bagus, tetapi sayangnya menyebabkan masalah dengan acar.
Baczek
14
Meskipun jawaban ini bagus, dimungkinkan untuk mengejek datetime tanpa membuat kelas: stackoverflow.com/a/25652721/117268
Emil Stenström
Bagaimana Anda mengembalikan datetimeinstance ke nilai aslinya? dengan deepcoppy?
Oleg Belousov
5
Jauh lebih mudah dilakukan:patch('mymodule.datetime', Mock(today=lambda: date(2017, 11, 29)))
Victor Gavro
1
Lebih mudah untuk dilakukan @patch('module_you_want_to_test.date', Mock( today=Mock(return_value=datetime.date(2017, 11, 29)))).
Jonhy Beebop
163

Opsi lain adalah menggunakan https://github.com/spulec/freezegun/

Pasang itu:

pip install freezegun

Dan gunakan itu:

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

Ini juga memengaruhi panggilan datetime lainnya dalam panggilan metode dari modul lain:

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

Dan akhirnya:

$ python main.py
# 2012-01-01
Mehdi Behrooz
sumber
13
Perpustakaan yang sangat berguna
Shaun
3
Anda mungkin juga mencoba python-libfaketime jika Anda melihat tes freezegun Anda berjalan lambat.
Simon Weber
Pustaka yang bagus, tetapi sayangnya tidak berfungsi dengan baik dengan Google App Engine NDB / Datastore.
Brandones
Saya suka bahwa "freezegun" adalah nama perpustakaan. Saya sangat suka Python devs! :-D
MikeyE
Bekerja, tetapi frezegun tampaknya lambat, terutama jika Anda memiliki logika yang rumit dengan beberapa panggilan untuk saat ini.
Andrey Belyak
115

Untuk apa nilainya, dokumen Mock berbicara tentang datetime.date.today khusus, dan mungkin untuk melakukan ini tanpa harus membuat kelas dummy:

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
sumber
2
Ini tidak benar-benar bekerja untuk saya. Meskipun saya menghargai upaya menemukan entri.
Pradyot
8
apa artinya "mymodule" di fungsi tambalan?
seufagner
4
Menemukan tautan di sini di bawah "mengejek Parsial"
Leo C Han
3
@seufagner mymodule dijelaskan dengan cara yang agak membingungkan di voidspace.org.uk/python/mock/patch.html#where-to-patch . Tampaknya jika modul Anda digunakan from datetime import datemaka itu adalah nama modul di mana from datetime import datedan panggilan untuk date.today()muncul
danio
1
Terima kasih. Bekerja! Contoh: dengan mock.patch ('tests.views.datetime') sebagai mock_date: mock_date.today.return_value = datetime.datetime (2016, 9, 18) mock_date.side_effect = lambda * args, ** kw: date (* args , ** kw)
Latrova
36

Saya kira saya datang sedikit terlambat untuk ini tetapi saya pikir masalah utama di sini adalah bahwa Anda menambal datetime.date.today secara langsung dan, menurut dokumentasi, ini salah.

Anda harus menambal referensi yang diimpor dalam file tempat fungsi yang diuji, misalnya.

Katakanlah Anda memiliki file functions.py di mana Anda memiliki yang berikut ini:

import datetime

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

maka, dalam ujian Anda, Anda harus memiliki sesuatu seperti ini

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

Semoga ini bisa membantu sedikit.

iferminm
sumber
Ini terlihat sangat menarik, tetapi saya tidak bisa menjalankannya (melempar a NameError: name 'datetime' is not defined). Dari mana datetime.strptimereferensi Mock(return_value=...)berasal jika Anda tidak mengimpor datetimedalam file pengujian Anda? PEMBARUAN: Tidak apa-apa, saya hanya melanjutkan dan mengimpor datetimemodul dalam file uji. Saya pikir triknya adalah bagaimana Anda menyembunyikan datetimereferensi dari file tes.
imrek
@DrunkenMaster Saya harus melihat contoh dari apa yang Anda lakukan dan referensi mana yang Anda ejek. yang Anda lakukan import datetimeatau from datetime import strptime? jika Anda melakukan yang pertama, Anda harus mengejek datetimedan melakukannya mocked_datetime.strptime.return_value = whatever, adalah yang kemudian, Anda harus langsung mengejek referensi strptime dalam file tempat tinggal metode yang diuji.
iferminm
@ israelord Yang ingin saya katakan adalah bahwa potongan kode terakhir Anda (file tes) tidak ada impor untuk referensi datetime untuk membuat Mock(return_value=datetime...)pekerjaan.
imrek
32

Untuk menambah solusi Daniel G :

from datetime import date

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

Ini menciptakan kelas yang, ketika dipakai, akan mengembalikan objek datetime.date yang normal, tetapi yang juga bisa diubah.

@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
sumber
2
Berhati-hatilah di sini - Anda harus menggunakan versi dari, jika tidak, Anda akan mendapatkan keanehan jika Anda menggunakan datetime.date (atau datetime atau lainnya). IE - kedalaman stack tercapai ketika panggilan palsu baru Anda sendiri.
Danny Staple
Anda tidak akan mengalami masalah itu jika objek palsu ada di modulnya sendiri: dpaste.com/790309 . Meskipun, meskipun berada dalam modul yang sama dengan fungsi yang dipermainkan, itu tidak mengimpor date/ datetimesendiri, ia menggunakan variabel yang tersedia secara global, jadi seharusnya tidak ada masalah: dpaste.com/790310
eternicode
penjelasan yang kurang singkat dapat ditemukan di sini: williamjohnbert.com/2011/07/…
ezdazuzena
9

Saya menghadapi situasi yang sama beberapa hari yang lalu, dan solusi saya adalah mendefinisikan fungsi dalam modul untuk diuji dan hanya mengejek itu:

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

Hari ini saya mengetahuinya FreezeGun , dan sepertinya menutupi kasus ini dengan indah

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

Cara termudah bagi saya adalah melakukan ini:

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)

PERHATIAN untuk solusi ini: semua fungsi dari datetime moduledari target_moduleakan berhenti bekerja.

frx08
sumber
1
Ini sangat bagus dan ringkas. Garis datetime_mock.now = Mock(return_value=datetime(1999, 1, 1)itu bahkan bisa dipersingkat menjadi datetime_mock.now.return_value = datetime(1999, 1, 1). Alih-alih memulai tambalan dengan start(), pertimbangkan untuk menggunakan with patch(...):manajer konteks untuk memastikan bahwa datetimeberperilaku teratur (tidak terhalang) lagi ketika tes Anda berakhir.
Dirk
Selalu mendukung solusi yang memanfaatkan perpustakaan
bawaan
@ frx08 Bolehkah saya tahu cara mengatur ulang ejekan ini? Maksud saya cara mendapatkan datetime.datetime.now()unmocked ^^?
Nam G VU
Nah setelah mencoba menggunakan tiruan ini - satu PERHATIAN untuk solusi ini adalah semua fungsi dari datetime moduledari target_moduleakan berhenti bekerja.
Nam G VU
1
Setuju @ frx08 dengan () akan menyamarkan rasa sakit. Meskipun di dalam blok itu semua misalnya tanggal, timedelta akan berhenti bekerja. Bagaimana jika sekarang kita perlu diejek tetapi matematika tanggal masih berjalan? Maaf, kita harus memiliki .sekarang () diejek tidak hanya seluruh modul datetime.
Nam G VU
7

Anda dapat menggunakan pendekatan berikut, berdasarkan pada solusi Daniel G. Yang satu ini memiliki keuntungan tidak melanggar jenis memeriksa 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)

Pada dasarnya, kami mengganti datetime.datekelas berbasis-C dengan subclass python kami sendiri, yang menghasilkan datetime.dateinstance asli dan menjawab isinstance()pertanyaan persis seperti aslidatetime.date .

Gunakan itu sebagai manajer konteks dalam pengujian Anda:

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)

Pendekatan serupa dapat digunakan untuk mengejek datetime.datetime.now()fungsi.

Andrey Lebedev
sumber
Saya tidak yakin ini bekerja di Python 2.7. Saya mendapatkan kedalaman rekursi maksimum RuntimeError dengan __instancecheck__metode ini.
Dan Loewenherz
Ini memang bekerja di Python 2.7, dan itu memecahkan masalah saya dengan contoh ketik, terima kasih!
Karatheodory
4

Secara umum, Anda akan memiliki datetimeatau mungkindatetime.date diimpor ke modul di suatu tempat. Cara yang lebih efektif untuk memperolok metode ini adalah dengan menambalnya pada modul yang mengimpornya. Contoh:

a.py

from datetime import date

def my_method():
    return date.today()

Kemudian untuk pengujian Anda, objek tiruan itu sendiri akan diteruskan sebagai argumen ke metode pengujian. Anda akan mengatur tiruan dengan nilai hasil yang Anda inginkan, dan kemudian memanggil metode Anda dalam pengujian. Maka Anda akan menegaskan bahwa metode Anda melakukan apa yang Anda inginkan.

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

Sebuah kata peringatan. Sangat mungkin untuk pergi ke laut dengan mengejek. Ketika Anda melakukannya, itu membuat tes Anda lebih lama, lebih sulit untuk dipahami, dan tidak mungkin untuk dipertahankan. Sebelum Anda mengejek metode sesederhana datetime.date.today, tanyakan pada diri Anda apakah Anda benar - benar perlu mengejeknya. Jika pengujian Anda singkat dan langsung ke sasaran dan berfungsi dengan baik tanpa mengejek fungsi, Anda mungkin hanya melihat detail internal kode yang Anda uji daripada objek yang perlu Anda tiru.

jpmc26
sumber
2

Berikut cara lain untuk mengejek datetime.date.today()dengan bonus tambahan yang sisa datetimefungsi terus bekerja, karena objek tiruan dikonfigurasi untuk membungkus datetimemodul asli :

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

Perhatikan wraps=datetimeargumen untuk mock.patch()- ketika foo_modulemenggunakan datetimefungsi lain selain date.today()mereka akan diteruskan ke datetimemodul yang dibungkus asli .

Tuan
sumber
1
Jawaban yang bagus, sebagian besar tes di mana Anda perlu mengejek tanggal Anda harus menggunakan modul datetime
Antoine Vo
1

Beberapa solusi dibahas di http://blog.xelnor.net/python-mocking-datetime/ . Singkatnya:

Objek tiruan - Sederhana dan efisien tetapi merusak isinstance () memeriksa:

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

Kelas tiruan

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)

Digunakan sebagai:

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

Saya menerapkan metode @ user3016183 menggunakan dekorator khusus:

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

Saya pikir itu mungkin membantu seseorang suatu hari ...

DainDwarf
sumber
0

Dimungkinkan untuk mengejek fungsi datetime modul tanpa menambahkanside_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
sumber
0

Bagi Anda yang menggunakan pytest dengan mocker di sini adalah bagaimana saya mengejek datetime.datetime.now()yang sangat mirip dengan pertanyaan aslinya.

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)

Pada dasarnya tiruan harus diatur untuk mengembalikan tanggal yang ditentukan. Anda tidak dapat menambal objek datetime secara langsung.

Daniel Butler
sumber
0

Saya membuat pekerjaan ini dengan mengimpor datetimesebagai realdatetimedan mengganti metode yang saya butuhkan di tiruan dengan metode nyata:

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

Anda dapat mengejek datetimemenggunakan ini:

Dalam modul sources.py:

import datetime


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

Di Anda 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
sumber
apa yang ada sourcesdi dekorator Anda?
Elena
Dear @elena, agak sulit untuk mengingat apa yang saya pikirkan hampir satu tahun yang lalu)). Saya kira saya maksudkan hanya modul apa pun dari sumber aplikasi kami - hanya kode aplikasi Anda.
MTMobile
0

CPython benar-benar mengimplementasikan modul datetime menggunakan Lib / datetime.py -Python murni dan Modul C-dioptimalkan / _datetimemodule.c . Versi C-dioptimalkan tidak dapat ditambal tetapi versi pure-Python bisa.

Di bagian bawah implementasi pure-Python di Lib / datetime.py adalah kode ini:

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

Kode ini mengimpor semua definisi C-dioptimalkan dan secara efektif menggantikan semua definisi pure-Python. Kita bisa memaksa CPython untuk menggunakan implementasi murni-Python dari modul datetime dengan melakukan:

import datetime
import importlib
import sys

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

Dengan menetapkan sys.modules["_datetime"] = None, kami memberi tahu Python untuk mengabaikan modul yang dioptimalkan-C. Kemudian kami memuat kembali modul yang menyebabkan impor dari_datetime gagal. Sekarang definisi pure-Python tetap dan dapat ditambal secara normal.

Jika Anda menggunakan Pytest maka sertakan cuplikan di atas di conftest.py dan Anda dapat menambal datetimeobjek secara normal.

GrantJ
sumber