4
\$\begingroup\$

I want to create a pytest with a fake utcnow, but also I need to preserve the functionality of all other datetime methods. Simple example here:

import datetime as dt


class FakeTime(dt.datetime):
    fake_time = None

    @classmethod
    def utcnow(cls):
        return cls.fake_time


def str_2_time(str_dt: str) -> dt.datetime:
    """Shortcut to do convert the string to datetime"""
    return dt.datetime.strptime(str_dt, '%Y-%m-%d %H:%M')


def test_patch_datetime():
    for utc_time in ['2019-01-01 10:00', '2019-02-01 13:00', '2019-03-01 16:00']:
        FakeTime.fake_time = str_2_time(utc_time)
        dt.datetime = FakeTime
        assert dt.datetime.utcnow() == str_2_time(utc_time)

Is this the right way?

The method str_2_time just need to show that all other methods of the datetime works fine.

\$\endgroup\$
  • \$\begingroup\$ I highly recommend this lib for mocking now in python tests github.com/spulec/freezegun \$\endgroup\$ – Anentropic Mar 6 at 14:25
  • \$\begingroup\$ @Anentropic thank you, but if i will need more options i will add the package to the project \$\endgroup\$ – Bear Brown Mar 6 at 14:44
6
\$\begingroup\$

According to this, subclassing datetime.datetime seems the way to go.

There is no use for the str_2_time method though. You can easily inline this, or even simpler, just use the datetime.datetime constructor:

def test_patch_datetime():
    for utc_time in [
        dt.datetime(2019, 1, 1, 10),
        dt.datetime(2019, 2, 1, 13),
        dt.datetime(2019, 3, 1, 16),
    ]:
        FakeTime.fake_time = utc_time
        dt.datetime = FakeTime
        assert dt.datetime.utcnow() == utc_time

You should be aware that this can have side effects in other parts of your code, so it might be needed to replace it back with the original class after the test method:

def test_patch_datetime():
    datetime_orig = dt.datetime

    utc_times = [
        dt.datetime(2019, 1, 1, 10),
        dt.datetime(2019, 2, 1, 13),
        dt.datetime(2019, 3, 1, 16),
    ]
    for utc_time in utc_times:
        FakeTime.fake_time = utc_time
        dt.datetime = FakeTime
        assert dt.datetime.utcnow() == utc_time
    dt.datetime = datetime_orig
    # print(dt.datetime.utcnow())
    assert dt.datetime.utcnow() > max(utc_times)
\$\endgroup\$
  • \$\begingroup\$ thank you, but the method str_2_time just need to show that all other methods of the datetime works fine. \$\endgroup\$ – Bear Brown Mar 6 at 8:36
2
\$\begingroup\$

Usually, I do:

  1. Separate module, for example utils.py, that contains:
from datetime import datetime

def get_utcnow() -> datetime:
    return datetime.utcnow()
  1. Use this function everywhere in my code.
  2. Add the mocking fixture in tests/conftest.py:
from datetime import datetime, timedelta

import pytest

from .. import utils

@pytest.fixture
def mock_utcnow(monkeypatch):
    now = datetime.min

    def wrapped(delta=0.0):
        when = now + timedelta(delta)
        monkeypatch.setattr(utils, "get_utcnow", lambda: when)
        return when

    return wrapped
  1. Now it's easy to use it in your tests:
def test(mock_utcnow):
    now = mock_utcnow()
    new_now = mock_utcnow(0.1)

Additionally, with this fixture you can set the returning value with desired offset.

Hope it helps.

\$\endgroup\$
  • \$\begingroup\$ thank you for the answer, but i don't understand how it should help for my case. \$\endgroup\$ – Bear Brown Mar 6 at 14:48
  • \$\begingroup\$ If I understand you right, you want utcnow to return fake datetime. So, using code from my answer: 1. You will use utils.get_utcnow in your real code, not in the tests. 2. In the tests you'll use fixture, that mocks utils.get_utcnow. After you call mocking function, every call of utils.get_utcnow in your real code will return fake datetime. \$\endgroup\$ – S. Zobov Mar 7 at 9:43
  • \$\begingroup\$ But with the code from my answer you'll get datetime.min by default, and after you can increase its value by adding timedelta, e.g. by calling mock_utcnow(0.1). \$\endgroup\$ – S. Zobov Mar 7 at 9:52
  • \$\begingroup\$ If you want to start not with a datetime.min, you can extend fixture with this But it requires more code in your tests. \$\endgroup\$ – S. Zobov Mar 7 at 9:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.