dcramer/mock-django

Support query.annotate()

slinkp opened this issue · 1 comments

Thanks for mock-django! I've been experimenting with using it to avoid database access in unit tests.

I've run into a few cases where the mock queryset doesn't quite support features of the django ORM.

Here's one... while trying to write a test method for some code that does Model.objects.all.annotate(foo='bar'),
and using ModelMock instances for my result objects from a ManagerMock,
the code under test fails because it expects to find the attributes added by annotate() and those are not part of
the ModelMock's spec. In production, annotate() works as expected so the code succeeds.

Here's the code:

    @property 
    def submission_sets(self): 
        for submission_set in models.SubmissionSet.objects.all().annotate(count=Count('children')):
            submission_sets[submission_set.place_id].append({
                'type': submission_set.submission_type,
                'count': submission_set.count,
                'url': reverse('submission_collection', kwargs={
                    'place_id': submission_set.place_id,
                    'submission_type': submission_set.submission_type
                })
            })
        return submission_sets

My test method looked like:

    def submission_sets_non_empty(self):
        from ..resources import models, PlaceResource
        from mock_django.managers import ManagerMock
        mock_manager = ManagerMock(models.SubmissionSet.objects,
                                   ModelMock(models.SubmissionSet,),
                                   ModelMock(models.SubmissionSet)
                                   )
        with mock.patch.object(models.SubmissionSet, 'objects', mock_manager):
            assert_equal(PlaceResource().submission_sets,  '....expected result goes here ...')

This blows up with this exception:

ERROR: sa_api.tests.test_resources.TestPlaceResource.submission_sets_non_empty
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/pw/builds/shareabouts/20120120/local/lib/python2.7/site-packages/nose/case.py", line 197, in runTest
    self.test(*self.arg)
  File "/home/pw/builds/shareabouts/20120120/sa-service/src/sa_api/tests/test_resources.py", line 87, in submission_sets_non_empty
    assert_equal(PlaceResource().submission_sets, '... results here...')
  File "/home/pw/builds/shareabouts/20120120/sa-service/src/sa_api/utils.py", line 73, in get
    x = self._property_cache[f] = f(self)
  File "/home/pw/builds/shareabouts/20120120/sa-service/src/sa_api/resources.py", line 90, in submission_sets
    'count': submission_set.count,
  File "/home/pw/builds/shareabouts/20120120/local/lib/python2.7/site-packages/mock.py", line 647, in __getattr__
    raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'count'

I had a go at implementing annotate() for mock_django.query ... my idea was to check if spec_set was True, if so add the desired attribute names to the spec, and then do setattr() on the objects in question;
but I admit I was baffled by the SharedMock magic, first because I didn't get what the "Shared" meant so I didn't realize that m.iterator.side_effect also applies to eg. m.annotate; and then once I realized that, I got stuck because there seems to be no access to the call args anywhere inside m, and without those I've got nothing to work with.

I would be happy to submit a patch but I could use a clue how to work with SharedMock... it strikes me as being maybe overly magical given that not all methods of QuerySet should return QuerySets.

Hacked around a bit; looks like more work than I have time to give right now.
My workaround for now is an alternative ModelMock factory that lets me set attributes when I make them.