Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use Mock library to mock a Django ForeignKey value?

I have a model and I'm trying to test validation without invoking the database layer. Rather than describe with words I'll just post up some example code. The issue here is the ForeignKey relationship to Bar, which isn't relevant to what I'm trying to test but is stopping me from running the test that I want.

First, myapp/models.py:

from django.core.exceptions import ValidationError
from django.db import models


class BadFooError(ValidationError):
    pass


class Bar(models.Model):
    description = models.CharField(max_length=20)


class Foo(models.Model):
    bar = models.ForeignKey(Bar)

    a_value = models.IntegerField()

    b_value = models.BooleanField()

    def clean(self):
        super(Foo, self).clean()
        if self.b_value and self.a_value > 50:
            raise BadFooError("No good")

Next, myapp/tests.py:

from unittest import TestCase

from mock import MagicMock

from . import models


class SimpleTest(TestCase):

    def test_avalue_bvalue_validation(self):
        foo = models.Foo()
        foo.a_value = 30
        foo.b_value = True
        foo.bar = MagicMock(spec=models.Bar)
        self.assertRaises(models.BadFooError, foo.full_clean)

    def test_method_2(self):
        foo = models.Foo()
        foo.a_value = 30
        foo.b_value = True
        foo.bar = MagicMock()
        foo.__class__ = models.Bar
        self.assertRaises(models.BadFooError, foo.full_clean)

    def test_method_3(self):
        foo = models.Foo()
        foo.a_value = 30
        foo.b_value = True
        # ignore it and it will go away ...??
        self.assertRaises(models.BadFooError, foo.full_clean)

Lastly, the output of python manage.py test myapp

EEE
======================================================================
ERROR: test_avalue_bvalue_validation (myapp.tests.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "~/sandbox/myapp/tests.py", line 14, in test_avalue_bvalue_validation
    foo.bar = MagicMock(spec=models.Bar)
  File "~/dsbx/local/lib/python2.7/site-packages/django/db/models/fields/related.py", line 408, in __set__
    instance._state.db = router.db_for_write(instance.__class__, instance=value)
  File "~/dsbx/local/lib/python2.7/site-packages/django/db/utils.py", line 142, in _route_db
    return hints['instance']._state.db or DEFAULT_DB_ALIAS
  File "~/dsbx/local/lib/python2.7/site-packages/mock.py", line 658, in __getattr__
    raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute '_state'

======================================================================
ERROR: test_method_2 (myapp.tests.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "~/sandbox/myapp/tests.py", line 21, in test_method_2
    foo.bar = MagicMock()
  File "~/dsbx/local/lib/python2.7/site-packages/django/db/models/fields/related.py", line 405, in __set__
    self.field.name, self.field.rel.to._meta.object_name))
ValueError: Cannot assign "<MagicMock id='31914832'>": "Foo.bar" must be a "Bar" instance.

======================================================================
ERROR: test_method_3 (myapp.tests.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "~/sandbox/myapp/tests.py", line 29, in test_method_3
    self.assertRaises(models.BadFooError, foo.full_clean)
  File "/usr/lib/python2.7/unittest/case.py", line 471, in assertRaises
    callableObj(*args, **kwargs)
  File "~/dsbx/local/lib/python2.7/site-packages/django/db/models/base.py", line 926, in full_clean
    raise ValidationError(errors)
ValidationError: {'bar': [u'This field cannot be null.']}

----------------------------------------------------------------------
Ran 3 tests in 0.003s

FAILED (errors=3)
Creating test database for alias 'default'...
Destroying test database for alias 'default'...

So my question is... wat do?

like image 276
Dan Passaro Avatar asked May 30 '13 23:05

Dan Passaro


1 Answers

In my unit tests, I simply assign _state to a new Mock instance, as in this small change to your first example unit test:

def test_avalue_bvalue_validation(self):
    foo = models.Foo()
    foo.a_value = 30
    foo.b_value = True
    bar = Mock(spec=models.Bar)
    bar._state = Mock()
    foo.bar = bar
    self.assertRaises(models.BadFooError, foo.full_clean)

However, to test your validation as a black box, I would pull the validation code into a separate method on your model which I would call from inside the clean() method. You can then unit test this validation code specifically. You will still need to do the _stage = Mock() assignment so you can create your instance of Foo, but at least you will be minimising the calls into Django.

like image 60
kinygos Avatar answered Sep 30 '22 18:09

kinygos