Error with Django assertJSONEqual when unit testing view - django

Similar to this question, I'm having problems testing a view with Django's unit testing framework. My view is very simple: it processes a form, adds an object to the database, and returns a JSONResponse. The test is equally simple, but I keep getting "First argument is not valid JSON: ''. The code actually works in my application; it just doesn't seem to work when unit testing. Any help is appreciated.
EDIT:
Traceback
======================================================================
ERROR: tearDownClass (zoning_intake.tests.AddActionTypeTest)
Traceback (most recent call last):
File "C:\Virtual\Django18\lib\site-packages\django\test\testcases.py", line 96
2, in tearDownClass
cls._rollback_atomics(cls.cls_atomics)
AttributeError: type object 'AddActionTypeTest' has no attribute 'cls_atomics'
======================================================================
FAIL: test_post_add_action_type_succeeds (zoning_intake.tests.AddActionTypeTest)
Traceback (most recent call last):
File "C:\Hg\sdcgis\zoning_intake\tests.py", line 26, in test_post_add_action_t
ype_succeeds
self.assertJSONEqual(response.content,{'result':'Success', 'msg':'Success'})
File "C:\Virtual\Django18\lib\site-packages\django\test\testcases.py", line 68
9, in assertJSONEqual
self.fail("First argument is not valid JSON: %r" % raw)
AssertionError: First argument is not valid JSON: ''
Ran 1 test in 10.757s
FAILED (failures=1, errors=1)
Preserving test database for alias 'default'...
Preserving test database for alias 'other'...
My view:
form = ActionTypeForm(request.POST)
if form.is_valid():
action = form.cleaned_data['action']
new_type = CaseRequestActionType(action=action)
new_type.save()
return JsonResponse({'result':'Success', 'msg':'Success'})
else:
return JsonResponse({'result':'Fail', 'msg':'An unknown error occurred'})
My test:
class AddActionTypeTest(TestCase):
if django.VERSION[:2] >= (1, 7):
# Django 1.7 requires an explicit setup() when running tests in PTVS
#classmethod
def setUpClass(cls):
django.setup()
def test_post_add_action_type_fails(self):
response = self.client.post(reverse('zoning:add_action_type'))
self.assertEqual(response.status_code, 302)
self.assertJSONEqual(force_text(response.content), {'result':'Fail', 'msg':'An unknown error occurred'})

So it turns out that the issue is very simple, and the 302 status code is the key to understanding why I had this issue. I have the #login_required decorator on my view, so when I ran the test WITHOUT having logged in a user, I'm redirected to my login view. Since the login view returns html, not JSON, my response is not valid JSON, and the status code returns 302 instead of the expected 200. I needed to override the setUp method to create a user in the database and then call login in the test itself in order for my test to work properly and my status code to return 200. Thanks to #Shang Wang for assistance
Complete View:
#login_required
def add_action_type(request):
if request.method == 'GET':
...
else:
form = ActionTypeForm(request.POST)
if form.is_valid():
action = form.cleaned_data['action']
new_type = CaseRequestActionType(action=action)
new_type.save()
return JsonResponse({'result':'Success', 'msg':'Success'})
else:
return JsonResponse({'result':'Fail', 'msg':'An unknown error occurred'})
Updated test:
class AddActionTypeTest(TestCase):
#classmethod
def setUp(self):
self.user = User.objects.create_user(username='shawn', email='shawn#...com', password='top_secret')
def test_post_add_action_type_fails(self):
self.client.login(username=self.user.username, password='top_secret')
response = self.client.post(reverse('zoning:add_action_type'))
self.assertJSONEqual(force_text(response.content), {'result':'Fail', 'msg':'An unknown error occurred'})

Related

Select related executes queries

I can't make select_related work with the following configuration:
Models:
class Text(models.Model):
author = models.ForeignKey('authors.Author',
on_delete=models.SET_NULL,
blank=True,
null=True)
Author model:
class Author(models.Model):
name = models.CharField(max_length=200)
def get_absolute_url(self):
kwargs = {'pk': self.pk}
return reverse('author-detail', kwargs=kwargs)
View
In a view, I use the select_related function to avoid hitting the db when querying for text's authors e.g.:mytext.author:
class TextsViewTest(TestCase):
def text_view(request,
pk,
template_name='texts/detail.html'):
source_text = Text.objects.select_related('author').get(pk=pk)
return render(request, template_name,
{
'source': source_text,
})
Test
According to select_related it shouldn't hit the database when accessing the Text.author relationship, but when testing it with:
def test_layout_content_header__uses_prefetched_relationships(self):
author = Author.objects.create(name="foobar")
source_text = Text.objects.create(author=author)
context = {'source': source_text}
with self.assertNumQueries(0):
from django.template.loader import render_to_string
rendered = render_to_string("text/_content_header.html", context)
template
text/content_header.html:
{% if source.author %} by <em>{{source.author.name}}</em>{% endif %}
Output
./manage test texts.test_views shows a hit:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_layout_content_header__uses_prefetched_relationships (author.tests.test_views.TextsViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/.../text/tests/test_views.py", line 1035, in test_layout_content_header__uses_prefetched_relationships
source_text.author
File "/.../lib/python3.6/site-packages/django/test/testcases.py", line 80, in __exit__
'%d. %s' % (i, query['sql']) for i, query in enumerate(self.captured_queries, start=1)
AssertionError: 1 != 0 : 1 queries executed, 0 expected
Captured queries were:
1. SELECT "authors_author"."id", "authors_author"."name", FROM "authors_author" WHERE "authors_author"."id" = 1
----------------------------------------------------------------------
Ran 1 test in 0.489s
FAILED (failures=1)
Destroying test database for alias 'default'...
Any ideas?
It looks like you are not using your view's code inside the test. Try either to copy same query into your test, e.g.:
context = {'source': Text.objects.select_related('author').get(pk=source_text.pk)}
with self.assertNumQueries(0):
from django.template.loader import render_to_string
rendered = render_to_string("text/_content_header.html", context)
Or reuse the view code (it seems to be declared in the Test Case, right?)
with self.assertNumQueries(1):
self.text_view(MagicMock(), source_text.pk)
Although you might need to specify bit more advanced request mock, e.g. using the RequestFactory

How to pass extra keyword arguments when using Django RequestFactory?

I'm trying to write a simple unit test for a view but I'm having trouble passing extra keyword arguments to the view when I'm using RequestFactory to set up the request.
To start, here's the urlpattern:
# app/urls.py
# Example URL: localhost:8000/run/user/1/foo
urlpatterns = [
url(r'^user/(?P<uid>\d+)/(?P<uname>\w+)/$',
views.user_kw,
name='user-kw'),
]
Here's the view I'm testing:
# app/views.py
def user_kw(request, *args, **kwargs):
uid = kwargs['uid']
uname = kwargs['uname']
return render(request, 'run/user.html', context)
Finally, here's the test:
# app/tests.py
def test_user_kw(self):
factory = RequestFactory()
# ???
request = factory.post('user/')
response = views.user_kw(request)
self.assertEqual(response.status_code, 200)
As you might expect, when I run the test, I get this error:
======================================================================
ERROR: test_user_kw (run.tests.TestViews)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/jones/code/django/testing/run/tests.py", line 53, in test_user_kw
response = views.user_kw(request, {"uid": 1, "uname": "foobar"})
File "/Users/jones/code/django/testing/run/views.py", line 28, in user_kw
uid = kwargs['uid']
KeyError: 'uid'
----------------------------------------------------------------------
The Django documentation on the RequestFactory object doesn't discuss this situation. I looked at the RequestFactory code itself but I couldn't figure out how to set up the object to account for the two keyword arguments contained in the URL. I also couldn't find anything online addressing this situation.
I should add that I did manage to write a test for the case in which I used positional arguments and it works:
def test_user_pos(self):
factory = RequestFactory()
request = factory.post('user/')
response = views.user_pos(request, 1, 'foo')
self.assertEqual(response.status_code, 200)
I just can't figure out how to rewrite the test for keyword arguments. Perhaps I've been looking at the problem for too long and the answer is staring me in the face, but I just don't see it.
You can pass keyword arguments to the user_pos method the normal way:
response = views.user_kw(request, uid=1, uname='foo')
Your error message shows that you tried:
response = views.user_kw(request, {"uid": 1, "uname": "foobar"})
This isn't passing keyword arguments, it's passing a dictionary as a positional argument. Note that you can use ** to unpack the dictionary:
response = views.user_kw(request, **{"uid": 1, "uname": "foobar"})

Authenticate user in Selenium test

I'm testing user authentication in Django project with Selenium webdriver, and created a test:
class EditorBlogTest(StaticLiveServerTestCase):
def setUp(self):
self.browser = webdriver.Chrome()
def tearDown(self):
self.browser.quit()
def create_session_store(self):
user = User.objects.create(email='a#b.com', username='TestUser')
engine = import_module(settings.SESSION_ENGINE)
store = engine.SessionStore()
store[SESSION_KEY] = user.pk
store.save()
return store
def test_user_authenticated(self):
store = self.create_session_store()
self.browser.get(self.live_server_url + '/dashboard/')
self.browser.add_cookie({
'name': settings.SESSION_COOKIE_NAME,
'value': store.session_key,
'path': '/',
})
self.browser.get(self.live_server_url + '/dashboard/blog/add/')
hello_box = self.browser.find_element_by_id('hello')
self.assertIsNotNone(self.browser.get_cookie('sessionid'))
self.assertEqual('Hello, TestUser', hello_box.text)
in template there is a line:
<div id="hello">Hello, {{ user }}</div>
But the test failed:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_session_creation (functional_tests.dashboard.test_blog.EditorBlogTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/django/my_project/functional_tests/dashboard/test_blog.py", line 26, in test_user_authenticated
self.assertEqual('Hello, TestUser', hello_box.text)
AssertionError: 'Hello, TestUser' != 'Hello, AnonymousUser'
- Hello, TestUser
+ Hello, AnonymousUser
----------------------------------------------------------------------
Ran 1 test in 2.406s
FAILED (failures=1)
Destroying test database for alias 'default'...
Does anyone have an idea what's wrong with it?
I've made a deeper research of the topic and found that session has nothing to do with RequestContext, that contains {{ user }} variable (as well as the rest context). So, the easiest way to authenticate a user in Selenium test is to open login page on the site and fill the form with test user credentials. After that my test managed to pass.

Django test client, not creating models (--keepdb option being used)

I'm trying to setup some models in the test database and then post to a custom form which includes a file upload.
Nothing seems to be persisting in the database, and I'm unsure about why the test when I perform the POST is sending back a 200 response? With follow=False, shouldn't it be a 302?
Also, when I try to look for the model in the database, it finds nothing.
And when I look at the database when using the --keepdb option, nothing is there either?
What am I doing wrong?
class ImportTestCase(TestCase):
remote_data_url = "http://test_data.csv"
local_data_path = "test_data.csv"
c = Client()
password = "password"
def setUp(self):
utils.download_file_if_not_exists(self.remote_data_url, self.local_data_path)
self.my_admin = User.objects.create_superuser('jonny', 'jonny#testclient.com', self.password)
self.c.login(username=self.my_admin.username, password=self.password)
def test_create_organisation(self):
self.o = Organization.objects.create(**{'name': 'TEST ORG'})
def test_create_station(self):
self.s = Station.objects.create(**{'name': 'Player', 'organization_id': 1})
def test_create_window(self):
self.w = Window.objects.create(**{'station_id': 1})
def test_create_component(self):
self.c = Component.objects.create(**{
'type': 'Player',
'window_id': 1,
'start': datetime.datetime.now(),
'end': datetime.datetime.now(),
'json': "",
'layer': 0}
)
def test_csv_import(self):
"""Testing that standard csv imports work"""
self.assertTrue(os.path.exists(self.local_data_path))
with open(self.local_data_path) as fp:
response = self.c.post('/schedule/schedule-import/create/', {
'component': self.c,
'csv': fp,
'date_from': datetime.datetime.now(),
'date_to': datetime.datetime.now()
}, follow=False)
self.assertEqual(response.status_code, 200)
def test_record_exists(self):
new_component = Component.objects.all()
self.assertTrue(len(new_component) > 0)
And the test results
Using existing test database for alias 'default'...
.....[]
F
======================================================================
FAIL: test_record_exists (schedule.tests.ImportTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests.py", line 64, in test_record_exists
self.assertTrue(len(new_component) > 0)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 6 tests in 1.250s
FAILED (failures=1)
The --keepdb option means that the database is kept. That means it's quicker to run the tests again because you don't have to recreate the table.s
However, each test method in a TestCase class is run in a transaction which is rolled back after the method has finished. Using --keepdb doesn't change this.
This means that your object created in test_create_component will not be seen by the test_record_exists test. You can either create the object inside the test_record_exists method, or in the setUp method or setUpTestData classmethod.

HttpResponse object becomes string when passed to assertContains

I have a strange problem in a Django template test. When the test executes my view, the view returns an HttpResponse object. However, when I then pass that response object to the Django TestCase assertContains method, the response object becomes a string. Since this string doesn't have a 'status_code' attribute like a response object does, the test fails. Here's my code:
template_tests.py
from django.test import TestCase
from django.test.client import RequestFactory
class TestUploadMainPhotoTemplate(TestCase):
def setUp(self):
self.factory = RequestFactory()
def test_user_selects_non_jpeg_photo_file(self):
"""
User is trying to upload a photo file via a form
with an ImageField. However, the file doesn't have
a '.jpg' extension so the form's is_valid function, which
I've overridden, flags this as an error and returns False.
"""
with open('photo.png') as test_photo:
request = self.factory.post(reverse('upload-photo'),
{'upload_photo': '[Upload Photo]',
'photo': test_photo})
kwargs = {'template': 'upload_photo.html'}
response = upload_photo(request, **kwargs)
# pdb.set_trace()
self.assertContains(response, 'Error: photo file must be a JPEG file')
When I run this code in the debugger and do 'type(response)' before I call assertContains, I can see that 'response' is a HttpResponse object. However, when assertContains is called, I get this error:
AttributeError: 'str' object has no attribute 'status_code'
I set an additional breakpoint in the assertContains method at the location .../django/test/testcases.py:638:
self.assertEqual(response.status_code, status_code...
At this point, when I do 'type(response)' again, I see that it has become a string object and doesn't have a status_code attribute. Can anyone explain what's going on? I've used this same test pattern successfully in a dozen other template tests and it worked in all of them. Could it have something to do with the fact that this test involves uploading a file?
Thanks.
I had a similar problem and solved it by looking at assertContains, it doesn't really help you but who knows ?
void assertContains( SimpleTestCase self, WSGIRequest response, text, count = ..., int status_code = ..., string msg_prefix = ..., bool html = ... )
Asserts that a response indicates that some content was retrieved
successfully, (i.e., the HTTP status code was as expected), and that
text occurs count times in the content of the response.
If count is None, the count doesn't matter - the assertion is true
if the text occurs at least once in the response.
Could it have something to do with the fact that this test involves uploading a file?
Sure, as I successfully wrote my test for a simple HttpResponse :
response = self.client.get('/administration/', follow=True)
self.assertContains(response, '<link href="/static/css/bootstrap.min.css" rel="stylesheet">',msg_prefix="The page should use Bootstrap")
So I am not really helping, but maybe this could help somebody a little.
I had a similar problem handling Json Response .
self.assertEquals(json.loads(response.content),{'abc': True})
Following fixed the problem for me.