Published
Updated
A short post explaining how to mock `django.utils.timezone.now` in unit tests.
I recently had to implement a Django model that represents a
competition with a start_time
field and an end_time
field.
To determine whether the competition is active, I get the current
time using django.utils.timezone.now()
and check if it's within
the time interval between start_time
and end_time
.
So far so good, the method looks like this:
# models.py
from django.db import models
from django.utils import timezone
class Competition(models.Model):
start_time = models.DateTimeField()
end_time = models.DateTimeField()
def is_active(self) -> bool:
now = timezone.now()
return now >= self.start_time and now < self.end_time
When we run the code, timezone.now()
will return a datetime
object representing the current time, and this is an issue
because unit tests should be deterministic and always yield the
same results. We cannot write tests that will yield different
results depending on the time they're run at.
This is where mocking comes in: in our unit tests we will mock
timezone.now()
so that it returns the same datetime
objects
every time.
# test_models.py
from datetime import datetime, timezone
from unittest.mock import patch
from .models import Competition
# `timezone.now` will now always return the value we want
@patch(
"django.utils.timezone.now",
lambda: datetime(2022, 1, 15, tzinfo=timezone.utc),
)
def test_Competition_is_active():
competition = Competition(
start_time=datetime(2021, 12, 1, tzinfo=timezone.utc),
end_time=datetime(2022, 1, 31, tzinfo=timezone.utc),
)
assert competition.is_active() is True
competition = Competition(
start_time=datetime(2021, 12, 1, tzinfo=timezone.utc),
end_time=datetime(2021, 1, 31, tzinfo=timezone.utc),
)
assert competition.is_active() is False
Now that we don't have to worry about the runtime value of
timezone.now
, we can run our CI/CD pipelines with the same
results on every run, and avoid unexpected failures.