Django ORM - mock values().filter() chain - python

I am trying to mock a chained call on the Djangos model.Manager() class. For now I want to mock the values() and filter() method.
To test that I created a little test project:
Create a virtual environment
Run pip install django mock mock-django nose django-nose
Create a project django-admin.py startproject mocktest
Create an app manage.py startapp mockme
Add django_nose and mocktest.mockme to INSTALLED_APPS (settings.py)
Add TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' to settings.py
To verfiy that everything is setup correctly I ran manage.py test. One test is run, the standard test Django creates when you create an app.
Next thing I did was to create a very simple model.
mockme/models.py
from django.db import models
class MyModel(models.Model):
name = models.CharField(max_length=50)
Next thing I did was to create a simple function that uses MyModel. That's the function I want to test later.
mockme/functions.py
from models import MyModel
def chained_query():
return MyModel.objects.values('name').filter(name='Frank')
Nothing special is happening here. The function is filtering the MyModel objects to find all instances where name='Frank'. The call to values() will return a ValuesQuerySet which will only contain the name field of all found MyModel instances.
mockme/tests.py
import mock
from django.test import TestCase
from mocktest.mockme.models import MyModel
from mocktest.mockme.functions import chained_query
from mock_django.query import QuerySetMock
class SimpleTest(TestCase):
def test_chained_query(self):
# without mocked queryset the result should be 0
result = chained_query()
self.assertEquals(result.count(), 0)
# now try to mock values().filter() and reeturn
# one 'Frank'.
qsm = QuerySetMock(MyModel, MyModel(name='Frank'))
with mock.patch('django.db.models.Manager.filter', qsm):
result = chained_query()
self.assertEquals(result.count(), 1)
The first assertEquals will evaluate as successful. No instances are returned since the model Manager is not mocked yet. When the second assertEquals is called I expect result to contain the MyModel instance I added as return value to the QuerySetMock:
qsm = QuerySetMock(MyModel, MyModel(name='Frank'))
I mocked the filter() method and not the values() method since I found it'll be the last evaluated call, though I am not sure.
The test will fail because the second result variable won't contain any MyModel instances.
To be sure that the filter() method is really mocked I added a "debug print" statement:
from django.db import models
print models.Manager.filter
which returned:
<SharedMock name='mock.iterator' id='4514208912'>
What am I doing wrong?

Try this:
import mock
from mocktest.mockme.models import MyModel
class SimpleTest(TestCase):
def test_chained_query(self):
my_model_value_mock = mock.patch(MyModel.objects, 'value')
my_model_value_mock.return_value.filter.return_value.count.return_value = 10000
self.assertTrue(my_model_value_mock.return_value.filter.return_value.count.called)

#Gin's answer got me most of the way there, but in my case I'm patching MyModel.objects, and the query I'm mocking looks like this:
MyModel.objects.filter(arg1=user, arg2=something_else).order_by('-something').first()
so this worked for me:
#patch('MyModel.objects')
def test_a_function(mock, a_fixture):
mock.filter.return_value.order_by.return_value.first.return_value = a_fixture
result = the_func_im_testing(arg1, arg2)
assert result == 'value'
Also, the order of the patched attributes matters, and must match the order you're calling them within the tested function.

Related

Can I define classes in Django settings, and how can I override such settings in tests?

We are using Django for Speedy Net and Speedy Match (currently Django 1.11.17, we can't upgrade to a newer version of Django because of one of our requirements, django-modeltranslation). I want to define some of our settings as classes. For example:
class UserSettings(object):
MIN_USERNAME_LENGTH = 6
MAX_USERNAME_LENGTH = 40
MIN_SLUG_LENGTH = 6
MAX_SLUG_LENGTH = 200
# Users can register from age 0 to 180, but can't be kept on the site after age 250.
MIN_AGE_ALLOWED_IN_MODEL = 0 # In years.
MAX_AGE_ALLOWED_IN_MODEL = 250 # In years.
MIN_AGE_ALLOWED_IN_FORMS = 0 # In years.
MAX_AGE_ALLOWED_IN_FORMS = 180 # In years.
MIN_PASSWORD_LENGTH = 8
MAX_PASSWORD_LENGTH = 120
PASSWORD_VALIDATORS = [
{
'NAME': 'speedy.core.accounts.validators.PasswordMinLengthValidator',
},
{
'NAME': 'speedy.core.accounts.validators.PasswordMaxLengthValidator',
},
]
(which is defined in https://github.com/speedy-net/speedy-net/blob/staging/speedy/net/settings/global_settings.py). And then in the models, I tried to use:
from django.conf import settings as django_settings
class User(ValidateUserPasswordMixin, PermissionsMixin, Entity, AbstractBaseUser):
settings = django_settings.UserSettings
(and then use attributes of settings, such as settings.MIN_USERNAME_LENGTH, in the class).
But it throws an exception
AttributeError: 'Settings' object has no attribute 'UserSettings'
(but it doesn't throw an exception if I use there a constant which is not a class).
This is the first problem. In the meantime, I defined instead:
from speedy.net.settings import global_settings as speedy_net_global_settings
class User(ValidateUserPasswordMixin, PermissionsMixin, Entity, AbstractBaseUser):
settings = speedy_net_global_settings.UserSettings
The second problem, is how do I override such settings in tests? For example, I use the following code:
from speedy.core.settings import tests as tests_settings
#override_settings(MAX_NUMBER_OF_FRIENDS_ALLOWED=tests_settings.OVERRIDE_MAX_NUMBER_OF_FRIENDS_ALLOWED)
in https://github.com/speedy-net/speedy-net/blob/staging/speedy/core/friends/tests/test_views.py. But if MAX_NUMBER_OF_FRIENDS_ALLOWED would be defined in the class UserSettings, how do I override it?
Django doesn't expect you to deviate much from its low-level design choices and it's usually a struggle to work around things that Django doesn't explicitly allow you to customize.
Django's settings object explicitly skips over any objects in your settings module with non-uppercase names. If you rename your class to USER_SETTINGS, it will work. If you really want to keep your object's original name a horrible solution would be to trick Django:
class UserSettings:
...
class AlwaysUppercaseStr(str):
def isupper(self):
return True
globals()[AlwaysUppercaseStr('UserSettings')] = globals().pop('UserSettings')
I have no idea if this is portable across Python implementations but it works with CPython's dir().
override_settings has no support for what you're trying to do so you will probably need to rewrite that class to allow the global settings object to be configurable.
Thanks to #Blender for the tip:
Django's settings object explicitly skips over any objects in your
settings module with non-uppercase names. If you rename your class to
USER_SETTINGS, it will work.
I was not aware that all the settings have to be uppercase. So I renamed class UserSettings to class USER_SETTINGS (although PyCharm doesn't like it), but I checked and it's also possible to add this code at the end of the file:
USER_SETTINGS = UserSettings
Without renaming the class.
As for my second question - how do I override such settings in tests? I added a file called utils.py:
def get_django_settings_class_with_override_settings(django_settings_class, **override_settings):
class django_settings_class_with_override_settings(django_settings_class):
pass
for setting, value in override_settings.items():
setattr(django_settings_class_with_override_settings, setting, value)
return django_settings_class_with_override_settings
(You can see it on https://github.com/speedy-net/speedy-net/blob/staging/speedy/core/base/test/utils.py)
And then in the tests:
from django.conf import settings as django_settings
from django.test import override_settings
from speedy.core.settings import tests as tests_settings
from speedy.core.base.test.utils import get_django_settings_class_with_override_settings
#override_settings(USER_SETTINGS=get_django_settings_class_with_override_settings(django_settings_class=django_settings.USER_SETTINGS, MAX_NUMBER_OF_FRIENDS_ALLOWED=tests_settings.OVERRIDE_USER_SETTINGS.MAX_NUMBER_OF_FRIENDS_ALLOWED))
def test_user_can_send_friend_request_if_not_maximum(self):
self.assertEqual(first=django_settings.USER_SETTINGS.MAX_NUMBER_OF_FRIENDS_ALLOWED, second=4)
I checked and I have to define another class (in this case, class django_settings_class_with_override_settings because if I change the class django_settings_class directly it also affects other tests which didn't use #override_settings.

How can I mock a method of a Django model manager?

Here is a little class (in myapp/getters.py):
from django.contrib.auth.models import User
class UserGetter:
def get_user(self):
return User.objects.get(username='username')
I would like to mock out the call to User.objects.get, return a MagicMock, and test that the method returns what I injected. In myapp/tests/tests_getters.py:
from unittest import TestCase
from django.contrib.auth.models import User, UserManager
from mock import patch, create_autospec
from myapp.getters import UserGetter
class MockTestCase(TestCase):
#patch('myapp.getters.User', autospec=True)
def test(self, user_class):
user = create_autospec(User)
objects = create_autospec(UserManager)
objects.get.return_value = user
user_class.objects.return_value = objects
self.assertEquals(user, UserGetter().get_user())
But when I run this test (with python manage.py test myapp.tests.tests_getters) I get
AssertionError:
<MagicMock name='User.objects().get()' spec='User' id='4354507472'> !=
<MagicMock name='User.objects.get()' id='4360679248'>
Why do I not get back the mock I injected? How can I write this test correctly?
I think this is your problem:
user_class.objects.return_value = objects
You instruct the mock to have a function "objects" that returns the objects on the right side.
But your code never calls any objects() function. It accesses the User.objects property, User is a Mock here, so User returns a new Mock on property access.

How to test a function for different database states in Python and Django? (avoiding repetitions)

I wrote unit tests first, then I made all the tests pass, now I am looking how to refactor the code to avoid repetitions.
I have a function which returns different values depending on the context. All context is extracted on-the-fly from the Django models.
Currently my code is structured like that:
from django.test import TestCase
class MyTest(TestCase):
def test_case1(self):
user = User.objects.create(username='user')
tested_class = MyClass(user)
Model1.objects.create(...) # one type of context
self.assertEqual(...) # test the class method for this type of context
def test_case2(self):
user = User.objects.create(username='user')
tested_class = MyClass(user)
Model2.objects.create(...) # another type of context
self.assertEqual(...) # test the class method for this type of context
def test_case3(self):
user = User.objects.create(username='user')
tested_class = MyClass(user)
Model1.objects.create(...) # yet another type of context
Model2.objects.create(...)
self.assertEqual(...) # test the class method for this type of context
Obviously, the code is quite repetitive: the first two lines are the same in each function.
My first idea was to use a shared setup function:
def setUp(self):
self.user = User.objects.create(username='user')
self.tested_class = MyClass(user)
but this solution didn't work: all model updates were shared, and tests became dependent on each other.
What I need instead is a clean state ("empty database") before starting each test.
What else can I try?
Why don't you just destroy all the objects you don't want in your teardown? Looks like Django allows you to do this type of thing pretty easily.
def tearDown(self):
User.objects.all().delete()

Django Rest Framework: How can I patch a serializer for unit testing, when it's used in the serializer_class attribute?

I have a serializer with an create() function. When I do a POST request, then I want to get this function called and create a new object. When I do it in the browser, it works and it also calls the function. But inside the test, it says that the function is not called. I think that I have done something wrong with the patch, because in the API it is only set as an serializer_class and the class is likely called somewhere inside the framework. Another thought was, that I do not need to test this, because it should be guaranteed by the rest_framework, that if I do it this way, the framework should call the function with the correct parameters.
# serializers.py
class FooSerializer(models.ModelSerializer):
class Meta:
...
def create(self, validated_data):
...
# apis.py
class FooAPI(generics.CreateAPIView):
serializer_class = FooSerializer
# tests.py
#patch('apis.FooSerializer'):
def test_that_create_is_called(self, mock):
mock.create = MagicMock()
mock.create.return_value = Foo() # Foo is my model
response = self.client.post('/foo', {name: 'Test'})
self.assertTrue(mock.create.called) # => Output says "False is not true"
Your current code is mocking the entire Serializer object, which is probably overkill and could stop the create method ever being called if it's expected to be called by internal logic on the serializer.
Instead you want to just patch a single method - like this:
#patch('apis.FooSerializer', 'create')
Now your test method receives the MagicMock object instance that has replaced the create method.
So your test method becomes:
def test_that_create_is_called(self, mock_method):
response = self.client.post('/foo', {name: 'Test'})
self.assertTrue(mock_method.called)

Django Splitting models from models.py file

I am trying to separate all the models from models.py file.What I am doing is mentioned in this link. But Problem is My one model is django.contrib.auth.user and I am wring one function in models.py as follows to generate token.
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
post_save.connect(create_user_profile, sender=User)
So how do I import that thing in _init_.py file as we are importing model as
from myapp.models.foo import Foo
You should only have either models.py or models/__init__.py, and it seem like you have both. One of these modules will probably shadow the other, so don't have both (i.e. get rid of the models.py)
If I'm following your question correctly, you don't need to import User into __init__.py. You just need to import it in the file in which you declare create_user_profile. The import itself is standard:
from django.contrib.auth.models import User
You can't import a command, but for example importing the function defined above it, ensures running the connect call. Function calls in the body of the models.py file are also executed of the same raeson (i.e. models are imported).
# p.py:
print "hello" # a command executed while importing anything
def x(): # a definition that can be imported
pass
# python shell
>>> from p import x
hello
>>>

Categories