Unit testing Django model with an image - Not quite understanding SimpleUploadedFile - django

I'm a testing noob and I'm trying to figure out how to write a test to confirm that a model form is valid and will generate a new instance of Post, which is a model that has an image field.
I looked some other SO posts, and it looks like I should be using SimpleUploadedFile to mock the image field. I'm having a hard time understanding how SimpleUploadedFile works (haven't found any straightforward documentation for this application), and different SO posts use some different looking syntax.
Am I supposed to point to a real file path to an actual image that is held somewhere in my Django app, or does this create a fake file to be used?
tests.py
class CreatePost(TestCase):
def test_create_post(self):
data = {
"content": "This is a post, I'm testing it out"
}
files_data = {
"image": SimpleUploadedFile(name='test_image.jpg', content=open(image_path, 'rb').read(), content_type='image/jpeg')
}
response = self.client.post("/new", data=data, files=files_data)
self.assertEqual(Post.objects.count(),1)
self.assertRedirects(response, 'index')
models.py
class Post(models.Model):
content = models.CharField(max_length=260)
timestamp = models.DateTimeField(auto_now_add=True)
posted_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts")
liked_by = models.ManyToManyField(User, blank=True, related_name="liked_posts")
image = models.ImageField(upload_to='uploads/', verbose_name='image')
def __str__(self):
return f"{self.posted_by} posted {self.content} at {self.timestamp}"
def is_valid_post(self):
return len(self.content) <= 260 and len(self.content) >= 0
class Post_form(ModelForm):
class Meta:
model = Post
fields = ['content', 'image']

I'm having a hard time understanding how SimpleUploadedFile works (haven't found any straightforward documentation for this application)
Take a look on source code of SimpleUploadedFile — it's a simple representation of a file, which just has content, size, and a name.
You don't need to point to a real file of real image (unless you want to). So you can replace real image (in your example — open(image_path, 'rb').read()) with fake data or even empty binary data b''
Also put all request fields under single data object.
And I don't find initialisation of client in your example.
Summarise, your test will finally look like on this one:
from django.test import Client, TestCase
class CreatePost(TestCase):
def setUp(self):
self.client = Client()
def test_create_post(self):
data = {
"content": "This is a post, I'm testing it out",
"image": SimpleUploadedFile(name='test_image.jpg', content=b'', content_type='image/jpeg')
}
response = self.client.post("/new", data=data)
self.assertEqual(Post.objects.count(),1)
self.assertRedirects(response, 'index')

Related

Input for field is missing even after i type the correct format on the API

I have coded the following:
models.py
class Job(models.Model):
datetime = models.DateTimeField(default=timezone.now)
combinedparameters = models.CharField(max_length = 1000)
serializers.py
class JobSerializers(serializers.ModelSerializer):
class Meta:
model = Job
fields = ['combinedparameters']
views.py
#api_view(['POST'])
def create_job(request):
job = Job()
jobserializer = JobSerializers(job, data = request.data)
if jobserializer.is_valid():
jobserializer.save()
return Response(jobserializer.data, status=status.HTTP_201_CREATED)
return Response(jobserializer.errors, status=status.HTTP_400_BAD_REQUEST)
Page looks like this:
But if i copy
{'device': 177, 'configuration': {'port_range': 'TenGigabitEthernet1/0/1,TenGigabitEthernet1/0/2,TenGigabitEthernet1/0/3,TenGigabitEthernet1/0/4,TenGigabitEthernet1/0/5', 'port_mode': 'Access', 'port_status': 'Disabled', 'port_param1': 'Test\\n1\\n2\\n3', 'port_param2': 'Test\\n1\\n2\\n3'}}
And click post, got error saying the single quotes have to be double quotes. So i changed it to :
{"device": 177, "configuration": {"port_range": "TenGigabitEthernet1/0/1,TenGigabitEthernet1/0/5", "port_mode": "Access", "port_status": "Disabled", "port_param1": "1\\n2\\n3", "port_param2": "1\\n2\\n3"}}
I clicked post again and this time the following error comes out:
I dont understand why is it happening. The reason why I key in the long format because this is the format i want to save in my database and this format is created upon saving from my html that creates the job
Postman:
Updated Postman:
Your post data object does not contain the required key "combinedparameters". I'm guessing that big object you copy into content is the string you want saved into the CharField combinedparameters? If that's the case you should structure your post data like this:
{
"combinedparameters": "{'device': 177, 'configuration': {'port_range': 'TenGigabitEthernet1/0/1,TenGigabitEthernet1/0/2,TenGigabitEthernet1/0/3,TenGigabitEthernet1/0/4,TenGigabitEthernet1/0/5', 'port_mode': 'Access', 'port_status': 'Disabled', 'port_param1': 'Test\\n1\\n2\\n3', 'port_param2': 'Test\\n1\\n2\\n3'}}"
}

Error when testing the view with a file upload

I would like to test a file upload from my view with the following function:
def test_post(self):
with open("path/to/myfile/test_file.txt") as file:
post_data = {
'description': "Some important file",
'file': file,
}
response = self.client.post(self.test_url, post_data)
self.assertEqual(response.status_code, 302)
document = Document.objects.first()
self.assertEqual(document.description, "My File")
self.assertEqual(document.filename, 'test_file.txt')
When I test the file upload on the actual website, it works. But when I run this test, I get the following error:
django.core.exceptions.SuspiciousFileOperation: Storage can not find
an available filename for "WHJpMYuGGCMdSKFruieo/Documents/eWEjGvEojETTghSVCijsaCINZVxTvzpXNRBvmpjOrYvKAKCjYu/test_file_KTEMuQa.txt".
Please make sure that the corresponding file field allows sufficient
"max_length".
Here is my form save method:
def save(self, commit=True):
instance = super(DocumentForm, self).save(commit=False)
instance.filename = self.cleaned_data['file'].name
if commit:
instance.save() # error occurs here
return instance
Seeing as it works on the actual website, I suspect it has something to do with the way I setup the file in the test; probably something small.
For the sake of brevity, I had removed irrelevant model fields from my original question. But when Ahtisham requested to see the upload_to attribute (which had a custom function), I removed those irrelevant fields and it worked!
So this is my original code (which didn't work) with the irrelevant fields:
def documents_path(instance, filename):
grant = instance.grant
client = grant.client
return '{0}/Grant Documents/{1}/{2}'.format(client.folder_name, grant.name, filename)
....
file = models.FileField(upload_to=documents_path)
But this works:
def documents_path(instance, filename):
return 'Documents/{0}'.format(filename)
The reason why it worked on the actual website is because it wasn't using long characters from the test fixtures. Seems like those fields that I thought were irrelevant for the question were actually quite important!
TL;DR I decreased the length of the custom document path.
I had the same error with an ImageField. The solution was adding max_length=255 in models.py:
image = models.ImageField(upload_to=user_path, max_length=255)
Then running python manage.py makemigrations and python manage.py migrate.

Populate model with metadata of file uploaded through django admin

I have two models,Foto and FotoMetadata. Foto just has one property called upload, that is an upload field. FotoMetadata has a few properties and should receive metadata from the foto uploaded at Foto. This can be done manually at the admin interface, but I want to do it automatically, i.e: when a photo is uploaded through admin interface, the FotoMetadata is automatically filled.
In my model.py I have a few classes, including Foto and FotoMetadata:
class Foto(models.Model):
upload = models.FileField(upload_to="fotos")
def __str__(self):
return '%s' %(self.upload)
class FotoMetadata(models.Model):
image_formats = (
('RAW', 'RAW'),
('JPG', 'JPG'),
)
date = models.DateTimeField()
camera = models.ForeignKey(Camera, on_delete=models.PROTECT)
format = models.CharField(max_length=8, choices=image_formats)
exposure = models.CharField(max_length=8)
fnumber = models.CharField(max_length=8)
iso = models.IntegerField()
foto = models.OneToOneField(
Foto,
on_delete=models.CASCADE,
primary_key=True,
)
When I login at the admin site, I have an upload form related to the Foto, and this is working fine. My problem is that I can't insert metadata at FotoMetadata on the go. I made a function that parse the photo and give me a dictionary with the info I need. This function is called GetExif is at a file called getexif.py. This will be a simplified version of it:
def GetExif(foto):
# Open image file for reading (binary mode)
f = open(foto, 'rb')
# Parse file
...
<parsing code>
...
f.close()
#create dictionary to receive data
meta={}
meta['date'] = str(tags['EXIF DateTimeOriginal'].values)
meta['fnumber'] = str(tags['EXIF FNumber'])
meta['exposure'] = str(tags['EXIF ExposureTime'])
meta['iso'] = str(tags['EXIF ISOSpeedRatings'])
meta['camera'] =str( tags['Image Model'].values)
return meta
So, basically, what I'm trying to do is use this function at admin.py to automatically populate the FotoMetadata when uploading a photo at Foto, but I really couldn't figure out how to make it. Does any one have a clue?
Edit 24/03/2016
Ok, after a lot more failures, I'm trying to use save_model in admin.py:
from django.contrib import admin
from .models import Autor, Camera, Lente, Foto, FotoMetadata
from fotomanager.local.getexif import GetExif
admin.site.register(Autor)
admin.site.register(Camera)
admin.site.register(Lente)
admin.site.register(FotoMetadata)
class FotoAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
# populate the model
obj.save()
# get metadata
metadados = GetExif(obj.upload.url)
# Create instance of FotoMetadata
fotometa = FotoMetadata()
# FotoMetadata.id = Foto.id
fotometa.foto = obj.pk
# save exposure
fotometa.exposure = metadados['exposure']
admin.site.register(Foto, FotoAdmin)
I thought it would work, or that I will have problems saving data to the model, but actually I got stucked before this. I got this error:
Exception Type: FileNotFoundError
Exception Value:
[Errno 2] No such file or directory: 'http://127.0.0.1:8000/media/fotos/IMG_8628.CR2'
Exception Location: /home/ricardo/Desenvolvimento/fotosite/fotomanager/local/getexif.py in GetExif, line 24
My GetExif function can't read the file, however, the file path is right! If I copy and paste it to my browser, it downloads the file. I'm trying to figure out a way to correct the address, or to pass the internal path, or to pass the real file to the function instead of its path. I'm also thinking about a diferent way to access the file at GetExif() function too. Any idea of how to solve it?
Solution
I solved the problem above! By reading the FileField source, I've found a property called path, which solve the problem. I also made a few other modifications and the code is working. The class FotoAdmin, at admin.py is like this now:
class FotoAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
# populate the model
obj.save()
# get metadata
metadados = GetExif(obj.upload.path)
# Create instance of FotoMetadata
fotometa = FotoMetadata()
# FotoMetadata.id = Foto.id
fotometa.foto = obj
# set and save exposure
fotometa.exposure = metadados['exposure']
fotometa.save()
I also had to set null=True at some properties in models.py and everything is working as it should.
I guess you want to enable post_save a signal
read : django signals
Activate the post_save signal - so after you save a FOTO you have a hook to do other stuff, in your case parse photometa and create a FotoMetadata instance.
More, if you want to save the foto only if fotometa succeed , or any other condition you may use , pre_save signal and save the foto only after meta foto was saved.

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

mocking a method on django model using post_save signal

So here's something I'm trying to figure out. I've got a method that is triggered by post_save
for this "Story" model. Works fine. What I need to do is figure out how to mock out the test, so I can fake the call and make assertions on my returns. I think I need to patch it somehow, but I've tried a couple different ways without much success. Best i can get is a object instance, but it ignores values I pass in.
I've commented in my test where my confusion lies. Any help would be welcome.
Here's my test:
from django.test import TestCase
from django.test.client import Client
from marketing.blog.models import Post, Tag
from unittest.mock import patch, Mock
class BlogTestCase(TestCase):
fixtures = [
'auth-test.json',
'blog-test.json',
]
def setUp(self):
self.client = Client()
def test_list(self):
# verify that we can load the list page
r = self.client.get('/blog/')
self.assertEqual(r.status_code, 200)
self.assertContains(r, "<h1>The Latest from Our Blog</h1>")
self.assertContains(r, 'Simple JavaScript Date Formatting')
self.assertContains(r, 'Page 1 of 2')
# loading a page out of range should redirect to last page
r = self.client.get('/blog/5/', follow=True)
self.assertEqual(r.redirect_chain, [
('http://testserver/blog/2/', 302)
])
self.assertContains(r, 'Page 2 of 2')
# verify that unpublished posts are not displayed
with patch('requests') as mock_requests:
# my futile attempt at mocking.
# creates <MagicMock> object but not able to call return_values
mock_requests.post.return_value = mock_response = Mock()
# this doesn't get to the magic mock object. Why?
mock_response.status_code = 201
p = Post.objects.get(id=5)
p.published = False
# post_save signal runs here and requests is called.
# Needs to be mocked.
p.save()
r = self.client.get('/blog/')
self.assertNotContains(r, 'Simple JavaScript Date Formatting')
Here's the model:
from django.db import models
from django.conf import settings
from django.db.models import signals
import requests
def update_console(sender, instance, raw, created, **kwargs):
# ignoring raw so that test fixture data can load without
# hitting this method.
if not raw:
update = instance
json_obj = {
'author': {
'alias': 'the_dude',
'token': 'the_dude'
},
'text': update.description,
}
headers = {'content-type': 'application/json'}
path = 'http://testserver.com:80/content/add/'
request = requests(path, 'POST',
json_obj, headers=headers,
)
if request.status_code < 299:
story_id = request.json().get('id')
if story_id:
# disconnect and reconnect signal so
# we don't enter recursion-land
signals.post_save.disconnect(
update_console,
sender = Story, )
update.story_id = story_id
update.save()
signals.post_save.connect(
update_console,
sender = Story, )
else:
raise AttributeError('Error Saving to console, '+ request.text)
class Story(models.Model):
"""Lets tell a story"""
story_id = models.CharField(
blank=True,
max_length=10,
help_text="This maps to the id of the post"
)
slug = models.SlugField(
unique=True,
help_text="This is used in URL and in code references.",
)
description = models.TextField(
help_text='2-3 short paragraphs about the story.',
)
def __str__(self):
return self.short_headline
# add/update this record as a custom update in console
signals.post_save.connect(update_console, sender = Story)
You need to patch requests in the module where it is actually used, i.e.
with patch('path.to.your.models.requests') as mock_requests:
mock_requests.return_value.status_code = 200
mock_requests.return_value.json.return_value = {'id': story_id'}
...
The documentation offers more detailed explanations on where to patch:
patch works by (temporarily) changing the object that a name points to with another one. There can be many names pointing to any individual object, so for patching to work you must ensure that you patch the name used by the system under test.
The basic principle is that you patch where an object is looked up, which is not necessarily the same place as where it is defined.
Here, you need to patch the name requests inside the models module, hence the need to provide its full path.