Problem with testing custom admin actions - django

Running into some problems when testing my custom admin actions.
First I can show you an example of a test that works and the actions it's testing.
custom action, Product model
#admin.action(description="Merge selected products")
def merge_products(self, request, queryset):
list_of_products = queryset.order_by("created_at")
list_of_products = list(list_of_products)
canonical_product = list_of_products[0]
list_of_products.remove(canonical_product)
for p in list_of_products:
for ep in p.external_products.all():
ep.internal_product = canonical_product
ep.save()
p.save()
canonical_product.save()
related test
class MockRequest(object):
pass
class ProductAdminTest(TestCase):
def setUp(self):
self.product_admin = ProductAdmin(model=Product, admin_site=AdminSite())
User = get_user_model()
admin_user = User.objects.create_superuser(
username="superadmin", email="superadmin#email.com", password="testpass123"
)
self.client.force_login(admin_user)
def test_product_merge(self):
self.boot1 = baker.make("products.Product", title="Filippa K boots", id=uuid4())
self.boot2 = baker.make("products.Product", title="Filippa K boots", id=uuid4())
self.external_product1 = baker.make(
"external_products.ExternalProduct", internal_product=self.boot1
)
self.external_product2 = baker.make(
"external_products.ExternalProduct", internal_product=self.boot2
)
self.assertEqual(self.boot1.external_products.count(), 1)
self.assertEqual(self.boot2.external_products.count(), 1)
request = MockRequest()
queryset = Product.objects.filter(title="Filippa K boots")
self.product_admin.merge_products(request, queryset)
self.assertEqual(self.boot1.external_products.count(), 2)
self.assertEqual(self.boot2.external_products.count(), 0)
Might not be the pretties test but it works, so does the action.
The code above works as it should but has been running into problems when trying to test an almost identical action but for another model.
custom action, Brand model
#admin.action(description="Merge selected brands")
def merge_brands(self, request, queryset):
qs_of_brands = queryset.order_by("created_at")
list_of_brands = list(qs_of_brands)
canonical_brand = list_of_brands[0]
for brand in list_of_brands:
if brand.canonical:
canonical_brand = brand
list_of_brands.remove(canonical_brand)
for brand in list_of_brands:
brand.canonical_brand = canonical_brand
brand.save()
for product in Product.objects.filter(brand=brand):
product.brand = canonical_brand
product.save()
canonical_brand.save()
related test
class MockRequest(object):
pass
def setUp(self):
self.brand_admin = BrandAdmin(model=Brand, admin_site=AdminSite())
User = get_user_model()
admin_user = User.objects.create_superuser(
username="superadmin", email="superadmin#email.com", password="testpass123"
)
self.client.force_login(admin_user)
def test_brand_merge(self):
self.brand1 = baker.make("brands.Brand", title="Vans")
self.brand1.canonical_brand = self.brand1
self.brand1.active = True
self.brand1.save()
self.brand2 = baker.make("brands.Brand", title="Colmar")
self.boot1 = baker.make("products.Product", brand=self.brand1, id=uuid4())
self.boot2 = baker.make("products.Product", brand=self.brand2, id=uuid4())
self.assertEqual(self.boot1.brand, self.brand1)
self.assertEqual(self.boot2.brand, self.brand2)
self.assertEqual(self.brand1.active, True)
self.assertEqual(self.brand2.active, False)
request = MockRequest()
queryset = Brand.objects.all()
self.brand_admin.merge_brands(request, queryset)
self.assertEqual(self.boot1.brand, self.brand1)
self.assertEqual(self.boot2.brand, self.brand1)
self.assertEqual(self.brand1.active, True)
self.assertEqual(self.brand2.active, False)
self.assertEqual(self.brand1.canonical_brand, self.brand1)
self.assertEqual(self.brand2.canonical_brand, self.brand1)
It's not really necessary how the code above works but I included it for context.
The product admin action works both when I try it manually and in the test suite. The brand action does work as it should when tested manually but in the test suite, it does nothing.
self.brand_admin.merge_brands(request, queryset)
does not change the tested objects in any way.
I have another custom admin action for the Brand model and it's the same problem there, works like a charm when I try it manually but it does nothing in the test suite. Because of those facts, my guess is that the problem is related to my admin setup in the test suite but it's identical to the admin setup used for the first test which works.
Any ideas?
Gladly provide more context if needed.

It looks like you need to refresh the objects from the Database, in your test self.brand1, boot1, etc are in memory and will not be automatically updated from the database, you need to get the new values from the database.
https://docs.djangoproject.com/en/4.0/ref/models/instances/#refreshing-objects-from-database
If you need to reload a model’s values from the database, you can use the refresh_from_db() method. When this method is called without arguments the following is done:
All non-deferred fields of the model are updated to the values currently present in the database.
Any cached relations are cleared from the reloaded instance.
After you call your merge_brands you should refresh each object.
self.boot1.refresh_from_db()
self.boot2.refresh_from_db()
# etc..

Related

How to check with pytest if one function was called inside Django form save method?

What i Have
Within the save method of the form are called two functions: generate_random_password and send_email, I need to know if these functions were called because generate_random_password assigns a password randomly to each new created user and send_email sends a email notification to the user with the credentials, the password generated and the user to login. It is important to know if these functions were executed correctly within save.
class UserAdminCreationForm(forms.ModelForm):
class Meta:
model = User
fields = ()
def save(self, commit: bool = True) -> U:
user = super(UserAdminCreationForm, self).save(commit=False)
data = self.cleaned_data
# Set random password for every new user
random_password = generate_random_password()
user.set_password(random_password)
# Send email confirmation with credentials to login
email = data.get("email")
html_message = render_to_string(
template_name="mails/user_creation_notification.html",
context={"email": email, "password": random_password},
)
# Strip the html tag. So people can see the pure text at least.
plain_message = strip_tags(html_message)
send_mail(
subject="Bienvenido a TodoTránsito",
message=plain_message,
recipient_list=[email],
html_message=html_message,
)
# Save into the DB the new user
if commit:
user.save()
return user
The problem
I'm using pytest to test my Django code, but I don't know how to assert if these functions were called.
From what you wrote in the comments, I would guess mocking both functions could make sense, as you probably don't really need the random password in your test. You could do something like:
from unittest import mock
#mock.patch('some_module.generate_random_password', return_value='swordfish')
#mock.patch('some_module.send_mail')
def test_save_user(mocked_send_mail, mocked_generate_password):
user_form = create_user_form() # whatever you do to create the form in the test
user_from.save()
mocked_generate_password.assert_called_once()
mocked_send_mail.assert_called_once()
# or mocked_send_mail.assert_called_once_with(...) if you want to check the parameters it was called with
Note that you have to make sure to mock the correct module, e.g. the one used in the tested code (see where to patch).
In this case generate_random_password is replaced with a mock that always returns the same password, and send_mail is replaced by a mock that does nothing except recording calls. Both mocks can be accessed via the arguments in the test, that are injected by the patch decorators (last decorator first, the argument names are arbitrary).
If you install pytest-mock, you get the mocker fixture, that gives you the same functionality and more. The same code would like like this:
def test_save_user(mocker):
mocked_generate_password = mocker.patch('some_module.generate_random_password', return_value='swordfish')
mocked_send_mail = mocker.patch('some_module.send_mail')
user_form = create_user_form() # whatever you do to create the form in the test
user_from.save()
mocked_generate_password.assert_called_once()
mocked_send_mail.assert_called_once()
If you now want to use the real generate_random_password, but still want to see if it was called, you can use mocker.spy instead:
def test_save_user(mocker):
mocked_generate_password = mocker.spy(some_module, 'generate_random_password')
mocked_send_mail = mocker.patch('some_module.send_mail')
user_form = create_user_form() # whatever you do to create the form in the test
user_from.save()
mocked_generate_password.assert_called_once()
mocked_send_mail.assert_called_once()
Note that you can achieve the same with unittest.mock.patch.object, but less convenient, in my opinion.

get() in Google Datastore doesn't work as intended

I'm building a basic blog from the Web Development course by Steve Hoffman on Udacity. This is my code -
import os
import webapp2
import jinja2
from google.appengine.ext import db
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
jinja_env = jinja2.Environment(loader = jinja2.FileSystemLoader(template_dir), autoescape = True)
def datetimeformat(value, format='%H:%M / %d-%m-%Y'):
return value.strftime(format)
jinja_env.filters['datetimeformat'] = datetimeformat
def render_str(template, **params):
t = jinja_env.get_template(template)
return t.render(params)
class Entries(db.Model):
title = db.StringProperty(required = True)
body = db.TextProperty(required = True)
created = db.DateTimeProperty(auto_now_add = True)
class MainPage(webapp2.RequestHandler):
def get(self):
entries = db.GqlQuery('select * from Entries order by created desc limit 10')
self.response.write(render_str('mainpage.html', entries=entries))
class NewPost(webapp2.RequestHandler):
def get(self):
self.response.write(render_str('newpost.html', error=""))
def post(self):
title = self.request.get('title')
body = self.request.get('body')
if title and body:
e = Entries(title=title, body=body)
length = db.GqlQuery('select * from Entries order by created desc').count()
e.put()
self.redirect('/newpost/' + str(length+1))
else:
self.response.write(render_str('newpost.html', error="Please type in a title and some content"))
class Permalink(webapp2.RequestHandler):
def get(self, id):
e = db.GqlQuery('select * from Entries order by created desc').get()
self.response.write(render_str('permalink.html', id=id, entry = e))
app = webapp2.WSGIApplication([('/', MainPage),
('/newpost', NewPost),
('/newpost/(\d+)', Permalink)
], debug=True)
In the class Permalink, I'm using the get() method on the query than returns all records in the descending order of creation. So, it should return the most recently added record. But when I try to add a new record, permalink.html (it's just a page with shows the title, the body and the date of creation of the new entry) shows the SECOND most recently added. For example, I already had three records, so when I added a fourth record, instead of showing the details of the fourth record, permalink.html showed me the details of the third record. Am I doing something wrong?
I don't think my question is a duplicate of this - Read delay in App Engine Datastore after put(). That question is about read delay of put(), while I'm using get(). The accepted answer also states that get() doesn't cause any delay.
This is because of eventual consistency used by default for GQL queries.
You need to read:
https://cloud.google.com/appengine/docs/python/datastore/data-consistency
https://cloud.google.com/appengine/docs/python/datastore/structuring_for_strong_consistency
https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore/
search & read on SO and other source about strong & eventual consistency in Google Cloud Datastore.
You can specify read_policy=STRONG_CONSISTENCY for your query but it has associated costs that you should be aware of and take into account.

model_mommy - user to assignment relationship

I'm finally setting up testing for my Django app, but I'm having difficulties getting started. I'm using model_mommy to create dynamic data for my tests, but have the following problem:
The view I'm testing is supposed to show me all the assignments a particular user has to complete. To test this, I want to create 500 assignments, log into the app and check if they are shown. So far I have the following test cases:
class TestLogin(TestCase):
def setUp(self):
self.client = Client()
user = User.objects.create(username='sam')
user.set_password('samspassword')
user.save()
def test_login(self):
self.client.login(username='sam', password='samspassword')
response = self.client.get('/')
print (response.content)
self.assertEqual(response.status_code, 200)
and
class TestShowAssignments(TestCase):
def setUp(self):
user_recipe = Recipe(User, username='sam', password="samspassword")
self.assignment = Recipe(Assignment,
coders = related(user_recipe))
self.assignments = self.assignment.make(_quantity=500)
def test_assignments(self):
self.assertIsInstance(self.assignments[0],Assignment)
self.assertEqual(len(self.assignments),500)
The first test passes fine and does what it should: TestLogin logs the user in and shows his account page.
The trouble starts with TestShowAssignments, which creates 500 assignments but if I look at the assignments with print (self.assignments[0].coders), I get auth.User.None. So it doesn't add the user I defined as a relation to the assignments. What might be important here is that the coders field in the model is a m2m field, which I tried to address by using related, but that doesn't seem to work.
What also doesn't work is logging in: if I use the same code I use for logging in during TestLogin in TestShowAssignments, I can't log in and see the user page.
So, my question: How do I use model_mommy to create Assignments and add them to a specific user, so that I can log in as that user and see if the assignments are displayed properly?
Do you want 500 Assignments that all have User "sam" as a single entry in the 'coders' field? If so, try:
from model_mommy import mommy
...
class TestShowAssignments(TestCase):
def setUp(self):
self.user = mommy.make(User, username='sam', password='samspassword')
self.assignments = mommy.make(Assigment, coders=[self.user], _quantity=500)

Only lists and tuples may be used in a list field Validation Error

Hi I am implementing test cases for my models.
I am using Mongoengine0.9.0 + Django 1.8
My models.py
class Project(Document):
# commented waiting for org-group to get finalize
project_name = StringField()
org_group = ListField(ReferenceField(OrganizationGroup, required=False))
My Serializers.py
class ProjectSerializer(DocumentSerializer):
class Meta:
model = Project
depth = 1
test.py file
def setUp(self):
# Every test needs access to the request factory.
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='jacob', email='jacob#jacob.com', password='top_secret')
def test_post_put_project(self):
"""
Ensure we can create new clients in mongo database.
"""
org_group = str((test_utility.create_organization_group(self)).id)
url = '/project-management/project/'
data = {
"project_name": "googer",
"org_group": [org_group],
}
##import pdb; pdb.set_trace()
factory = APIRequestFactory()
user = User.objects.get(username='jacob')
view = views.ProjectList.as_view()
# Make an authenticated request to the view...
request = factory.post(url, data=data,)
force_authenticate(request, user=user)
response = view(request)
self.assertEqual(response.status_code, 200)
When I am running test cases I am getting this error
(Only lists and tuples may be used in a list field: ['org_group'])
The complete Stack Trace is
ValidationError: Got a ValidationError when calling Project.objects.create().
This may be because request data satisfies serializer validations but not Mongoengine`s.
You may need to check consistency between Project and ProjectSerializer.
If that is not the case, please open a ticket regarding this issue on https://github.com/umutbozkurt/django-rest-framework-mongoengine/issues
Original exception was: ValidationError (Project:None) (Only lists and tuples may be used in a list field: ['org_group'])
Not getting why we cant pass object like this.
Same thing when I am posting as an request to same method It is working for me but test cases it is failing
The tests should be running using multipart/form-data, which means that they don't support lists or nested data.
You can override this with the format argument, which I'm guessing you probably want to set to json. Most likely your front-end is using JSON, or a parser which supports lists, which explains why you are not seeing this.

Django Modular Testing

I have an "ok" test suite now, but I'm wanting to improve it. What happens is that I'm having to repeat setting up (limiting models for an example) users, property, school, and city objects.
Here is an example of something I have now, which works (note: could be broken because of changes made to simplify the example, but the logic is what I'm after):
class MainTestSetup(TestCase):
def setUp(self):
self.manage_text = 'Manage'
User = get_user_model()
# set up all types of users to be used
self.staff_user = User.objects.create_user('staff_user', 'staff#gmail.com', 'testpassword')
self.staff_user.is_staff = True
self.staff_user.save()
self.user = User.objects.create_user('user', 'user#gmail.com', 'testpassword')
self.city = City.objects.create(name="Test Town", state="TX")
self.school = School.objects.create(city=self.city, name="RE Test University",
long=-97.1234123, lat=45.7801234)
self.company = Company.objects.create(name="Test Company", default_school=self.school)
def login(self):
self.client.login(username=self.user.username,
password='testpassword')
def login_admin(self):
self.client.login(username=self.staff_user, password="testpassword")
class MainViewTests(MainTestSetup):
def test_home(self):
url = reverse('home-list')
manage_url = reverse('manage-property')
anon_response = self.client.get(url)
self.assertEqual(anon_response.status_code, 200)
self.assertNotContains(anon_response, self.manage_text)
self.login_admin()
admin_response = self.client.get(url)
self.assertContains(admin_response, self.manage_text)
def test_search(self):
url = reverse('search')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
...more tests
As you can see the MainViewTest inherits the setUp and login functions from the MainTestSetup class. This works ok, but I have many apps and not all need to set up all models. What I've tried to do is set up a set of mixins to include things like User, School, Company only in the TestSetups that I need.
This MainTestSetup would turn into something like:
class SchoolMixin(object):
def setUp(self):
self.city = City.objects.create(name="Test Town", state="TX")
self.school = School.objects.create(city=self.city, name="RE Test University",
long=-97.1234123, lat=45.7801234)
class CompanyMixin(SchoolMixin):
def setUp(self):
self.company = Company.objects.create(name="Test Company", default_school=self.school)
class UserMixin(object):
def setUp(self):
User = get_user_model()
# set up all types of users to be used
self.staff_user = User.objects.create_user('staff_user', 'staff#gmail.com', 'testpassword')
self.staff_user.is_staff = True
self.staff_user.save()
self.user = User.objects.create_user('user', 'user#gmail.com', 'testpassword')
def login(self):
self.client.login(username=self.user.username,
password='testpassword')
def login_admin(self):
self.client.login(username=self.staff_user, password="testpassword")
class MainTestSetup(UserMixin, CompanyMixin, TestCase):
def setUp(self):
self.manage_text = 'Manage'
This would allow a lot more flexibility for my test suite - this is only a small example. It would allow me in other apps to only include the Mixins that are necessary. For example if company was not needed, I would include just the SchoolMixin from the above example.
I believe my problem here is with inhertance of the setUp function. I'm not sure how to inherit correctly (through super, or though something else?). I've tried using super but haven't been able to get it to work. I have to admit, I'm not that great with classes/mixins yet, so any help/pointers would be much appreciated.
You can simplify and reduce the amount of code you have by using 2 libraries: WebTest and FactoryBoy. You won't need these Mixins.
https://pypi.python.org/pypi/django-webtest
https://github.com/rbarrois/factory_boy
Do the change step by step:
1. Starts with WebTest so you can get rid of your login_ method (and you won't need to prepare the passwords as well). With WebTest, you can specify the logged-in user when you load a page. For instance you will replace:
self.login_admin()
admin_response = self.client.get(url)
with:
admin_response = = self.app.get(url, user=self.admin)
2. Then use factory_boy to create all the objects you need. For instance you will replace:
self.staff_user = User.objects.create_user('staff_user', 'staff#gmail.com', 'testpassword')
self.staff_user.is_staff = True
self.staff_user.save()
with:
self.staff_user = StaffFactory.create()
3. Mix it up. Get rid of self.admin. Replace it with:
admin = AdminFactory.create()
response = = self.app.get(url, user=admin)
Once you've done all that, your code is going to be a lot shorter and easier to read. You won't need these mixins at all. For example your SchoolMixin can be replaced like this:
self.city = City.objects.create(name="Test Town", state="TX")
self.school = School.objects.create(city=self.city, name="RE Test University",
long=-97.1234123, lat=45.7801234)
replaced with:
school = SchoolFactory.create()
That's because factories can automatically create related entities with "SubFactories".
Here is a complete really simple example of a test using factories: http://codeku.co/testing-in-django-1