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"})
Related
This is a bit complicated because I'm debugging some code written a long time ago in python 2.7
In progress of migrating to Python 3 (I know, I know) and facing this problem when trying to fix unit tests
The problem is I'm getting an error TypeError: object() takes no parameters
I'll list the functions below. I have to replace a lot of names of functions and objects. If you see an inconsistency in module names, assume it's typo.
First the class it's calling
class Parser(object):
def __init__(self, some_instance, some_file):
self._some_instance = some_instance
self.stream = Parser.formsomestream(some_file)
self.errors = []
#staticmethod
def formsomestream(some_file):
# return a stream
class BetterParser(Parser):
def parse(self):
# skip some steps, shouldn't relate to the problem
return details # this is a string
class CSVUploadManager(object):
def __init__(self, model_instance, upload_file):
self._model_instance = model_instance
self._upload_file = upload_file
# then bunch of functions here
# then.....
def _parse(self):
parser_instance = self._parser_class(self._model_instance, self._upload_file)
self._csv_details = parser_instance.parse()
# bunch of stuff follows
def _validate(self):
if not self._parsed:
self._parse()
validator_instance = self._validator_class(self._model_instance, self._csv_details)
# some attributes to set up here
def is_valid(self):
if not self._validated:
self._validate()
Now the test function
from somewhere.to.this.validator import MockUploadValidator
from another.place import CSVUploadManager
class TestSomething(SomeConfigsToBeMixedIn):
#mock.patch('path.to.BetterParser.parse')
#mock.patch('path.to.SomeValidator.__new__')
#mock.patch('path.to.SomeValidator.validate')
def test_validator_is_called(self, mock_validator_new, mock_parse):
mock_validator_new.return_value = MockUploadValidator.__new__(MockUploadValidator)
mock_parse.return_value = mock_csv_details
mock_validator_new.return_value = MockUploadValidator()
string_io = build_some_string_io_woohoo() # this returns a StringIO
some_file = get_temp_from_stream(string_io)
upload_manager = CSVUploadManager(a_model_instance, some_file)
upload_manager.is_valid() # this is where it fails and produces that error
self.assertTrue(mock_parse.called)
self.assertTrue(mock_validator_new.called)
validator_new_call_args = (SomeValidator, self.cash_activity, mock_csv_details)
self.assertEqual(mock_validator_new._mock_call_args_list[0][0], validator_new_call_args)
As you can see, the CSVUploadManager takes in the a django model instance and a file-like obj, this thing will trigger self._parser_class which calls BetterParser, then BetterParser does its things.
However, I'm guessing it's due to the mock, it returns TypeError: object() takes no parameters
My questions:
Why would this error occur?
Why only happening on python 3.x? (I'm using 3.6)
This also causes other tests (in different testcases) to fail when they would normally pass if I don't test them with the failed test. Why is that?
Is it really related to mocking? I'd assume it is because when I test on the server, the functionality is here.
EDIT: adding Traceback
Traceback (most recent call last):
File "/path/to/lib/python3.6/site-packages/mock/mock.py", line 1305, in patched
return func(*args, **keywargs)
File "/path/to/test_file.py", line 39, in test_validator_is_called:
upload_manager.is_valid()
File "/path/to/manager.py", line 55, in is_valid
self._validate()
File "/path/to/manager.py", line 36, in _validate
validator_instance = self._validator_class(self._model_instance, self._csv_details)
TypeError: object() takes no parameters
There should be 3 mock arguments, except self.
Like this:
#mock.patch('path.to.BetterParser.parse')
#mock.patch('path.to.SomeValidator.__new__')
#mock.patch('path.to.SomeValidator.validate')
def test_validator_is_called(self, mock_validate, mock_validator_new, mock_parse):
...
I am trying to create a very simple custom view for my application.
Lets imagine I have a simply model:
class Person(models.Model):
Name = models.CharField(max_length = 255)
pass
class Company(models.Model):
Title = models.CharField(max_length = 255)
pass
I would like to display a list of objects. On one url - list of person, on another - list of companies. So I create simply views:
def PersonListView (request):
_persons = Person.objects.all()
context = {
"object_list": _persons
}
return render (request, "PersonListView.html", context)
def CompanyListView (request):
_companies = Person.objects.all()
context = {
"object_list": _companies
}
return render (request, "CompanyListView.html", context)
Then add path to urls - /Person for list of persons and /Company for list of companies.
urlpatterns = [
path('/Person', PersonListView),
path('/Company', CompanyListView)
]
All working well. But I already have a questions:
Why my view function have a request variable, but I can call this function from urls without defining this variable?
Why I can't use this syntax path('/Person', PersonListView())? What is wrong with this brackets?
And another part of my question.
But then I decided to make some refactoring - I intended to use one view both for person and companies, so I have to pass a variable context to view function. And I basically know how to do it:
def ObjectList (request, _context):
_objects = _context.objects.all()
data = {
"object_list": _objects
}
return render (request, "ListView.html", data)
Here _context - concrete class (Person or Company).
My problem is in urls.py I have to call ObjectList and pass 2 variables:
_context - there is no problem here, I know how to do it
and request.
Here I faced a wall, because I don't understand how can I pass it to my function. If I just let it empty, I have an error
"ObjectList() missing 1 required positional argument: 'request'"
So how can I fix it? SHould I somehow pass it from my urls? Is there a way to use a view function with multiple arguments?
Here is the traceback:
Internal Server Error: /Entities/Person/
Traceback (most recent call last):
File "D:\Work\Python\virtualenvs\TestProject\lib\site-packages\django\core\handlers\exception.py", line 34, in inner
response = get_response(request)
File "D:\Work\Python\virtualenvs\TestProject\lib\site-packages\django\core\handlers\base.py", line 115, in _get_response
response = self.process_exception_by_middleware(e, request)
File "D:\Work\Python\virtualenvs\TestProject\lib\site-packages\django\core\handlers\base.py", line 113, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
TypeError: ObjectList() got multiple values for argument '_context'
[25/Jul/2019 00:25:37] "GET /Entities/Person/ HTTP/1.1" 500 62910
Your second question answers your first. You don't call the view from the URL; Django does that internally when it encounters a matching request. If you try to call it from there, then you get the error you see.
I don't understand though why you think you need to pass request from the URL in the third question. You don't, that's the whole point of your questions 1 and 2.
Your URL pattern should either capture the arguments for the view, or pass them as the third parameter to path. So, in your case, you could use a single pattern which captures the variable:
path('/<str:object_type>/', object_list, name='object_list')
and then:
def object_list(request, object_type):
types = {'Person': models.Person, 'Company': models.Company}
if object_type not in types:
raise Http404
objects = types[object_type].objects.all()
Alternatively, use two separate URL patterns and pass the type as the third parameter explicitly:
urlpatterns = [
path('/Person', object_list_view, {'object_type': Person}, name='person_list'),
path('/Company', object_list_view, {'object_type': Company}, name='company_list'),
]
and your view can be:
def object_list(request, object_type):
objects = object_type.objects.all()
Finally, though, for this very simple use case you should consider using a generic list view; then you wouldn't need to define any views at all:
from django.views.generic import ListView
urlpatterns = [
path('/Person', ListView.as_view(model=Person)),
path('/Company', ListView.as_view(model=Company))
]
I am using RequestFactory in a Django test, and I can't find the right way to access the session variable, and I'm getting the following error when I try
self.factory._session["zip_id"] or self.factory.session["zip_id"].
======================================================================
ERROR: test_middleware (dj_geo.tests.IPToZipMiddleWareTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "c:\dj_site_test\dj_geo\tests.py", line 36, in test_middleware
assert self.factory._session["zip_id"] != None
AttributeError: 'RequestFactory' object has no attribute '_session'
----------------------------------------------------------------------
#override_settings(MIDDLEWARE_CLASSES=(
'dj_geo.middleware.IPToZipMiddleWare'
))
class IPToZipMiddleWareTest(TestCase):
def test_middleware(self):
Zipcode.syncdb()
assert Zipcode.objects.all().count() > 0
self.factory = RequestFactory()
self.request = self.factory.get('/', {}, **{'REMOTE_ADDR':'108.31.178.99'})
assert self.factory._session["zip_id"] != None
assert self.factory._session["zip_id"] != ""
Save session information to request using your middleware:
from django.contrib.sessions.middleware import SessionMiddleware
request = RequestFactory().get('/')
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
You can use SessionMiddleware, indeed. However, its constructor requires a callback, as any middleware. The callback is provided by Django run-time in order to keep processing the middleware chain or to execute the view as soon as the chain reaches the end. Since we are not interested in view execution, for this case, you may do the following:
from django.contrib.sessions.middleware import SessionMiddleware
request = RequestFactory().get('/')
middleware = SessionMiddleware(lambda x: None)
middleware.process_request(request)
request.session.save()
By processing the request, session field will be added to it and you can keep going with your testing.
You may need to use the SessionMiddleware to process your request, then save it to store the session. You can refer to this article. I also don't think it's a good idea to access the protected attributes of the factory directly, like this self.factory._session["zip_id"], it will just get you into more problems. Goodluck!
You need to use Client for this instead of RequestFactory
self.factory = Client()
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'})
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.