Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

freeze_time doesn't freeze time of pytest fixtures #176

Closed
sliverc opened this issue Mar 22, 2017 · 22 comments
Closed

freeze_time doesn't freeze time of pytest fixtures #176

sliverc opened this issue Mar 22, 2017 · 22 comments

Comments

@sliverc
Copy link

sliverc commented Mar 22, 2017

Following test fails which shows that freeze_time only freezes the time of the test but not of dependent fixtures.

from datetime import date
import pytest
from freezegun import freeze_time

@pytest.fixture
def somedate():
    return date.today()

@freeze_time("2001-01-01")
def test_somedate(somedate):
    assert somedate == date.today()

Is there a way to freeze time for depending fixtures as well?

@spulec
Copy link
Owner

spulec commented Apr 13, 2017

I can't think of any way to do this. Pytest seems to evaluate the function before running the test.

Feel free to reopen if you can think of another way.

@spulec spulec closed this as completed Apr 13, 2017
@ktosiek
Copy link

ktosiek commented Apr 16, 2017

The best way to do this reliably with py.test would be using markers. Something like this: https://docs.pytest.org/en/latest/example/markers.html#marking-platform-specific-tests-with-pytest

@sliverc
Copy link
Author

sliverc commented Apr 25, 2017

markers would certainly be a good idea... question is though whether such should be part of freezegun directly or of a different pytest-freezegun plugin?

@ktosiek
Copy link

ktosiek commented Apr 25, 2017

Depends on who wants to maintain it.

I guess a separate plugin would be nice as it would be more explicit (and it's a few instructions added to each test, think of performance! ;-)).

Actually, I might whip something up later today - I've already written that code once, the core should be <50 lines. it's mostly packaging and testing.

@ktosiek
Copy link

ktosiek commented May 21, 2017

Well, that took longer than expected. I have a basic implementation here: https://github.com/ktosiek/pytest-freezetime/blob/master/pytest_freezegun.py. I'll put it on pypi when it's a bit more useful (mostly docs and access to the freezer in tests).

@sliverc
Copy link
Author

sliverc commented May 31, 2017

@ktosiek
Tested the marker and it works like a charm... Thanks.

@elcolie
Copy link

elcolie commented Nov 14, 2017

@sliverc
I do not understand. How do you use it suppose I want to solve the waring in #190

@sliverc
Copy link
Author

sliverc commented Nov 15, 2017

@elcolie I don't think your issue has anything to do with pytest. Rather a django issue potentially.

See https://docs.djangoproject.com/en/1.11/topics/i18n/timezones/.

What I usually do to create a timezone aware datetime in Django

from django.utils import timezone
d = timezone.now().replace(year=2017, ...)

@elcolie
Copy link

elcolie commented Nov 16, 2017

@sliverc

Github acts strange on my comment. Markup does not has a color on then. Neitherpython or bash works.

from poinkbackend.apps.commons.tests import *
from freezegun import freeze_time

from poinkbackend.apps.rewards.models import Reward
from poinkbackend.apps.commons.tests import branches


def test_queryset(branches):
    silom = Branch.objects.filter(name='Silom').first()
    from django.utils import timezone
    import datetime
    with freeze_time("2017-11-01 9:30:23", tz_offset=+7):
        # Thailand Time
        mommy.make(Reward, title='Teddy Bear', poink=100, description='Black Bear',
                   active_date=timezone.now(), expiry=timezone.now() + datetime.timedelta(days=5),
                   branch=silom)
    with freeze_time("2017-11-03 9:30:23", tz_offset=+7):
        today = timezone.now()
        assert 1 == Reward.objects.filter(expiry__gte=today).count()

    with freeze_time("2017-11-07 9:30:23", tz_offset=+7):
        today = timezone.now()
        assert 0 == Reward.objects.filter(expiry__gte=today).count()


def test_expired_reward(rewards):
    """Must not be able to get redeem"""
    with freeze_time("2099-11-01 9:30:23", tz_offset=+7):
        assert 0 == Reward.lives.count()

Here are my warning messges

===================================================================================== test session starts =====================================================================================
platform darwin -- Python 3.6.3, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 -- /Users/sarit/.pyenv/versions/3.6.3/envs/poink/bin/python
cachedir: .cache
Django settings: poinkbackend.config.settings.local (from ini file)
rootdir: /Users/sarit/Code/poink, inifile: pytest.ini
plugins: django-3.1.2
collected 2 items

poinkbackend/apps/rewards/tests.py::test_queryset PASSED
poinkbackend/apps/rewards/tests.py::test_expired_reward PASSED

====================================================================================== warnings summary =======================================================================================
poinkbackend/apps/rewards/tests.py::test_expired_reward
  /Users/sarit/.pyenv/versions/3.6.3/envs/poink/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1451: RuntimeWarning: DateTimeField Reward.active_date received a naive datetime (2017-11-01 09:30:23) while time zone support is active.
    RuntimeWarning)
  /Users/sarit/.pyenv/versions/3.6.3/envs/poink/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1451: RuntimeWarning: DateTimeField Reward.expiry received a naive datetime (2017-11-21 09:30:23) while time zone support is active.
    RuntimeWarning)

-- Docs: http://doc.pytest.org/en/latest/warnings.html
============================================================================ 2 passed, 2 warnings in 2.18 seconds =============================================================================

Please advise how to do it correct way.

@sliverc
Copy link
Author

sliverc commented Nov 20, 2017

Not tested but I assume if you create the time you want to freeze with timezone.now() and .replace() as my example above you can pass it on to freeze_time and warning should disappear.

d = timezone.now().replace(year=2017, ...)
with freeze_time(d):
   ...

@elcolie
Copy link

elcolie commented Dec 27, 2017

@sliverc
Thank you very much. For your response. After a month passed. I come back to code and found the problem. The problem comes from my fixture. I forgot to put tz_offset

I have to aware

  1. import datetime when I need datetime.timedelta this confuses me a bit with from datetime import datetime
  2. from django.utils import timezone. timezone.now() will put timezone according to settings.py for me
  3. with freeze_time("2017-11-01 9:30:23", tz_offset=+7): Always put tz_offset

fixture

@pytest.fixture
def rewards(branches):
    import datetime
    with freeze_time("2017-11-01 9:30:23", tz_offset=+7):
        branch = Branch.objects.first()
        mommy.make(Reward, title='LEGO Truck', poink=2, description='8-12yr', active_date=timezone.now(),
                   expiry=timezone.now() + datetime.timedelta(days=20), branch=branch)
        mommy.make(Reward, title='LEGO SpaceShip', poink=10, description='8-12yr', active_date=timezone.now(),
                   expiry=timezone.now() + datetime.timedelta(days=20), branch=branch)

tests.py

def test_expired_reward(rewards):
    """Must not be able to get redeem"""
    with freeze_time("2099-11-01 9:30:23", tz_offset=+7):
        assert 0 == Reward.objects.live_count()

@utapyngo
Copy link

@ktosiek, is pytest-freezegun still supported? There is no activity since November.

@boxed
Copy link
Contributor

boxed commented Sep 11, 2019

@utapyngo that doesn't sound very long to me. Why would a simple project like this warrant such a question?

@utapyngo
Copy link

A colleague of mine does not want to use pytest-freezegun because it is not supported. All PRs seem to be ignored.

@boxed
Copy link
Contributor

boxed commented Sep 11, 2019

There are just 4 PRs. In any case we use freezegun at work without using pytest-freezegun. It's extremely easy to set ut so no plug-in is needed imo.

@ktosiek
Copy link

ktosiek commented Sep 11, 2019

Yeah, I should take a look at those PRs. It works for me, in more than one project, if that's any consolation :-)

As @boxed mentioned, using freezegun without the plugin is easy, as long as you only need the time frozen inside the test (and not fixtures).

The plugin is handy if you want the whole fixture setup to use frozen time. Also, it's 50 lines of actual code - if you have a problem and I'm not responsive enough you can probably fix it (or even rewrite the whole thing).

@boxed
Copy link
Contributor

boxed commented Sep 11, 2019

We actually freeze the time of all tests unless specifically marked as wanting real time. We use simple fixtures to do that. I've written about it here https://link.medium.com/Rz2FbQCUTZ

@erik-megarad
Copy link

As far as I can tell you can accomplish this by having a fixture which freezes the time:

@pytest.fixture()
def frozen_time():
    with freezegun.freeze_time():
        yield

def test_something(frozen_time):
     before = datetime.now()
     time.sleep(1)
     after = datetime.now()
     assert before == after

@GordonSo
Copy link

GordonSo commented Jan 1, 2022

Great tips @subwindow !

Adding a fixture works like a charm and it's descriptive.
I expanded on your example to illustrate that the order of the fixture matters

@pytest.fixture
def given_its_monday():
    with freezegun.freeze_time("2022-1-3 12:00:00"):  # monday, 3rd jan
        yield

@pytest.fixture
def given_a_date():
    yield dt.datetime.utcnow()

def test_sample(given_its_monday, given_a_date):
    date = given_a_date
    # >>>> date: 2022-01-03 12:00:00
    assert(date.weekday() == 0)
    
def test_invalid_sample(given_a_date, given_its_monday):
    date = given_a_date
    # >>>> date: the actual current time outside of the freezetime

@justin-f-perez
Copy link

justin-f-perez commented Jun 7, 2022

I am using a similar approach to @GordonSo's most recent comment to test "interesting dates" so I can find bugs around e.g., Y2k. However, I find that when I pass --durations=N to pytest, pytest is being tricked into thinking these tests are taking extremely long amount of time. For example, if I do:

pytest --durations=3

Then I see:

================================= slowest 3 durations =================================
1654634423.31s setup    tests/core/test_utils.py::test_days_ago[now6-freeze_shotgun0-0-False]
1654634423.09s setup    tests/core/test_utils.py::test_days_ago[now4-freeze_shotgun0--1-True]
1654634423.05s setup    tests/core/test_utils.py::test_days_ago[now6-freeze_shotgun0--1-False]

The relevant test code:

from datetime import date, datetime, timedelta
from itertools import chain, product
from typing import Any, Optional

import pytest
from dateutil.parser import parse
from django.utils import timezone as tz
from freezegun import freeze_time

from core.utils import days_ago

_interesting_datetimes = (
    tz.localtime(),
    tz.make_aware(parse("Dec 31 1999, 23:59:59.9999")),
    tz.make_aware(parse("Jan 01 2000, 00:00:00.0000")),
)


@pytest.fixture(params=_interesting_datetimes)
def freeze_shotgun(request):
    with freeze_time(request.param):
        yield request.param


@pytest.fixture(
    params=chain(
        (None,), _interesting_datetimes, (tz.localdate(dt) for dt in _interesting_datetimes)
    )
)
def now(request):
    return request.param


def is_date(value):
    return isinstance(value, date)


def is_datetime(value: Any, tz_aware: Optional[bool] = None):
    if not isinstance(value, datetime):
        return False
    elif tz_aware is None:
        return True
    else:
        return tz.is_aware(value) == tz_aware


@pytest.mark.parametrize(
    "days,dt",
    product(
        {0, 1, -1},
        {True, False},
    ),
)
def test_days_ago(days, now, dt, freeze_shotgun):
    result = days_ago(days, now=now, dt=dt)

    assert is_datetime(result, tz_aware=True) if dt else is_date(result)

    now_date = tz.localdate(now) if is_datetime(now) else tz.localdate() if now is None else now
    result_date = tz.localdate(result) if is_datetime(result) else result
    assert is_date(result_date) and is_date(now_date)
    assert (now_date - result_date) == timedelta(days=days)

And the relevant implementation code:

from datetime import datetime, date
from typing import Union

from django.utils import timezone

def days_ago(days: int, now: Union[datetime, date, None] = None, dt=False) -> Union[datetime, date]:
    """
    Returns what date (or datetime) it was `days` from `now`.

    `days`: a positive integer for past dates,
                or a negative integer for future dates.
    `now`: the date to subtract `days` from. If None is given, the
           current date is used
    `dt`: return a datetime rather than a date. The time component will be
          equal to the current time of day, unless `now` is given and includes
          a time component (in which case, the given time component is preserved.)
    """
    if now is None:
        if dt:
            now = timezone.localtime()
        else:
            now = timezone.localdate()
    else:
        if dt and isinstance(now, date):
            now = timezone.make_aware(datetime.combine(now, timezone.localtime().time()))
        if not dt and isinstance(now, datetime):
            now = timezone.localtime(now).date()
    return now - timezone.timedelta(days=days)

@spulec do you have any thoughts on how I might "untrick" pytest? (Should I open a separate issue for this?)

@justin-f-perez
Copy link

Immediately after posting the above comment, I found pytest-durations which more accurately reports the time and is suitable for my use case. Thanks!

@ollemento
Copy link

I use this pattern which works great:

@pytest.fixture
@freeze_time("2022-09-01 12:00:00")
def now():
    return datetime.now()


@pytest.fixture
def yesterday(now):
    return now - timedelta(days=1)


@pytest.fixture
def tomorrow(now):
    return now + timedelta(days=1)


def test_time(yesterday, tomorrow):
    print(yesterday)
    print(tomorrow)
    assert False
--------------------------- Captured stdout call ----------------------------
2022-08-31 12:00:00
2022-09-02 12:00:00

Note that the order of the decorators matter.
Placing @freeze_time before @pytest.fixture doesn't work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants