Note that there are some explanatory texts on larger screens.

plurals
  1. POHow to mock datetime.date.today() method in Django 1.5.5 tests
    text
    copied!<p>I have a set of tests that rely on mocking out dates using the python mock library, and the <code>@mock.patch</code> decorator, together with the date mocking code sample found <a href="http://williamjohnbert.com/2011/07/how-to-unit-testing-in-django-with-mocking-and-patching/" rel="nofollow">here</a>. Using this, we have a FakeDate class:</p> <pre class="lang-py prettyprint-override"><code>class FakeDate(original_date): "A fake replacement for datetime.date that can be mocked for testing." def __new__(cls, *args, **kwargs): return original_date.__new__(original_date, *args, **kwargs) </code></pre> <p>And in our tests we have:</p> <pre class="lang-py prettyprint-override"><code>from datetime import date as real_date @mock.patch('datetime.date', FakeDate) def test_mondays_since_date(self): FakeDate.today = classmethod(lambda cls: real_date(2014, 1, 1)) # A Wednesday self.assertNotEqual(datetime.date.today(), real_date.today()) self.assertEqual(datetime.date.today().year, 2014) # and so on.. </code></pre> <p>Everything has been working up till I upgraded Django from 1.4.8 to 1.5.5. Unfortunately now the mock dates are causing tests to fail, but only on model save operations. The stack trace is as follows:</p> <pre class="lang-sh prettyprint-override"><code>File "/site-packages/django/db/models/base.py", line 546, in save force_update=force_update, update_fields=update_fields) File "/site-packages/django/db/models/base.py", line 650, in save_base result = manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw) File "/site-packages/django/db/models/manager.py", line 215, in _insert return insert_query(self.model, objs, fields, **kwargs) File "/site-packages/django/db/models/query.py", line 1675, in insert_query return query.get_compiler(using=using).execute_sql(return_id) File "/site-packages/django/db/models/sql/compiler.py", line 942, in execute_sql for sql, params in self.as_sql(): File "/site-packages/django/db/models/sql/compiler.py", line 900, in as_sql for obj in self.query.objs File "/site-packages/django/db/models/fields/__init__.py", line 304, in get_db_prep_save prepared=False) File "/site-packages/django/db/models/fields/__init__.py", line 738, in get_db_prep_value value = self.get_prep_value(value) File "/site-packages/django/db/models/fields/__init__.py", line 733, in get_prep_value return self.to_python(value) File "/site-packages/django/db/models/fields/__init__.py", line 697, in to_python parsed = parse_date(value) File "/site-packages/django/utils/dateparse.py", line 36, in parse_date match = date_re.match(value) TypeError: expected string or buffer </code></pre> <p>I've pdb'd my way into the Django source, and the problem seems to be here (in <a href="https://github.com/django/django/blob/stable/1.5.x/django/db/models/fields/__init__.py#L693" rel="nofollow">django/db/models/fields/<strong>init</strong>.py</a>:</p> <pre class="lang-py prettyprint-override"><code>def to_python(self, value): if value is None: return value if isinstance(value, datetime.datetime): if settings.USE_TZ and timezone.is_aware(value): # Convert aware datetimes to the default time zone # before casting them to dates (#17742). default_timezone = timezone.get_default_timezone() value = timezone.make_naive(value, default_timezone) return value.date() if isinstance(value, datetime.date): # &lt;-- This is the problem! return value try: parsed = parse_date(value) </code></pre> <p>The type equality expression is failing, hence the call to <code>parse_date</code> which actually raises the error. (i.e. the <code>value</code>, which is the date returned from my <code>FakeDate.today()</code> expression is not being seen as a standard lib <code>datetime.date</code> object.)</p> <p>So, I know where the problem lies, but what can I do to get around it? Mocking dates is critical to our application testing.</p> <p><strong>[EDIT 1: comparison of earlier version of Django]</strong></p> <p>To compare the above expression that fails in Django 1.5.5, the following is 1.4.8 (which does not fail):</p> <pre class="lang-py prettyprint-override"><code>def to_python(self, value): if value is None: return value if isinstance(value, datetime.datetime): return value.date() if isinstance(value, datetime.date): return value value = smart_str(value) try: parsed = parse_date(value) </code></pre> <p>i.e. they are the same. So why is one passing and the other failing - is this related to changes in the testrunner?</p> <p><strong>[EDIT 2: More debugging]</strong></p> <p>Digging a little further into the discrepancy:</p> <pre><code>&gt; /site-packages/django/db/models/fields/__init__.py(685)to_python() 684 import ipdb; ipdb.set_trace() --&gt; 685 if value is None: 686 return value ipdb&gt; value datetime.date(2012, 12, 7) ipdb&gt; isinstance(value, datetime.date) False ipdb&gt; type(value) &lt;type 'datetime.date'&gt; ipdb&gt; type(datetime.date) &lt;type 'type'&gt; ipdb&gt; datetime.date &lt;class 'testutils.FakeDate'&gt; ipdb&gt; datetime.datetime &lt;type 'datetime.datetime'&gt; </code></pre> <p><strong>[EDIT 3: located the problem]</strong></p> <p>I have discovered the discrepancy between the 1.4 and 1.5 branches, and it's not in the test runner. The key is the <code>value = smart_str(value)</code> line in the 1.4 branch. This is called before <code>parse_date</code>, and it converts our FakeDate into a string repr, which is parseable (e.g. '2012-05-09'). This is <em>not</em> called in the 1.5 version, which bombs.</p> <p>This is the sequence within the 1.4.x branch:</p> <pre><code># value = FakeDate(2012, 12, 31) # code fails the isinstance(datetime.date) test value = smart_str(value) # value is now a string '2012-12-31' parsed = parse_date(value) # inside parse_date we try a regex match match = date_re.match(value) # because we have called smart_str, this now parses as a date </code></pre> <p>The sequence within the 1.5.x branch does not include the smart_str conversion, and so the regex match fails, as the <code>value</code> argument in this case is a FakeDate object and not a string.</p> <p><strong>[EDIT 5: Bug submitted to Django]</strong></p> <p>I have submitted a bug to the Django issue tracker (<a href="https://code.djangoproject.com/ticket/21523" rel="nofollow">https://code.djangoproject.com/ticket/21523</a>) for this.</p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload