Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Fixed #20456: easier unit-testing for CBV #2368

Closed
wants to merge 1 commit into from

4 participants

@Keats

Added a class method to return an instance of a CBV.
This allows to unit test part of a CBV directly, without using
the client.

Note: as the ticket didn't have much discussion going on, I did a POC (only modified a single test to show how it would work), feedback on it needed before going further

@benoitbryon

I am ok with the as_instance() technique. Thanks @Keats for this proposal :)

In my dreams, calling the class constructor is the obvious way to get an instance of the class... but I know it is not so easy with current CBV implementation... so having some "as_instance()" method looks consistent with "as_view".

django/views/generic/base.py
@@ -76,6 +76,19 @@ def view(request, *args, **kwargs):
update_wrapper(view, cls.dispatch, assigned=())
return view
+ @classonlymethod
+ def as_instance(cls, request='', *args, **kwargs):

Can you explain why request='' by default? Is empty string a suitable default for a request instance?

What about having request as a required positional argument?

Or, if it is to support calls such as view = views.CustomTemplateView.as_instance() (no arguments), then what about initializing request like if request is None: request = RequestFactory().get('/fake')

@Keats
Keats added a note

I just didn't want to put that logic inside the as_instance, if people need a request they can provide one.

You don't always need a request depending on what you're testing so empty string would be ok

You don't always need a request depending on what you're testing...

Ok.

... so empty string would be ok

I'm not sure about it. If you do not want a default request instance, I'd prefer None instead of empty string.
That said, both None and empty string would trigger exceptions if an user uses view = CBV().as_instance() without passing a request, then he calls something that tries to use the request such as view.request.GET.

May someone else tell his opinion?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
tests/generic_views/test_base.py
@@ -294,11 +294,14 @@ def test_extra_template_params(self):
"""
A template view can be customized to return extra context.
"""
- response = self.client.get('/template/custom/bar/')
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context['foo'], 'bar')
- self.assertEqual(response.context['key'], 'value')
- self.assertIsInstance(response.context['view'], View)
+ request = RequestFactory().get('/dummy')
+ kwargs = {
+ 'foo': 'bar'
+ }
+ view = views.CustomTemplateView.as_instance(request, **kwargs)

A note about code length, because some users may say "using self.client.get() is more straightforward"...
We can also write the lines above (297-301) like this:

view = views.CustomTemplateView.as_instance(
    RequestFactory().get('/dummy'),
    foo='bar')

That said, the way they are written right now is fine too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@benoitbryon benoitbryon commented on the diff
tests/generic_views/urls.py
@@ -16,8 +16,6 @@
TemplateView.as_view()),
(r'^template/simple/(?P<foo>\w+)/$',
TemplateView.as_view(template_name='generic_views/about.html')),
- (r'^template/custom/(?P<foo>\w+)/$',
- views.CustomTemplateView.as_view(template_name='generic_views/about.html')),

This is one thing I like with this technique: we do not need to create and maintain URLconfs just for test purpose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@benoitbryon

Is it possible to call as_instance() within as_view()?

@Keats

You mean MyView.as_view().as_instance() ? That won't work.

Do you have an example of a usecase for that ?

@benoitbryon

I mean replacing as_view.view implementation (https://github.com/Keats/django/blob/as_instance/django/views/generic/base.py#L63-L68) by a call to as_instance.
It may look like this:

    @classonlymethod
    def as_view(cls, **initkwargs):
        # ... (unchanged)

        def view(request, *args, **kwargs):
            self = cls(**initkwargs).as_instance(request, *args, **kwargs)
            return self.dispatch(request, *args, **kwargs)

        # ... (unchanged)

Note: the implementation above does not work in the current pull-request, because in the current pull-request as_instance() is a class-only method... but you get the idea.

django/views/generic/base.py
@@ -76,6 +76,19 @@ def view(request, *args, **kwargs):
update_wrapper(view, cls.dispatch, assigned=())
return view
+ @classonlymethod

Perhaps not suitable, as mentioned in #2368 (comment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
django/views/generic/base.py
@@ -76,6 +76,19 @@ def view(request, *args, **kwargs):
update_wrapper(view, cls.dispatch, assigned=())
return view
+ @classonlymethod
+ def as_instance(cls, request='', *args, **kwargs):
+ """
+ Creates an instance of the class, useful for isolated testing.
+ You can create a request through RequestFactory and pass args/kwargs as
+ you would do for a reverse() call.
+ """
+ view = cls()
+ view.request = request

The "head" hack applied in as_view.view is missing, isn't it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
django/views/generic/base.py
@@ -76,6 +76,19 @@ def view(request, *args, **kwargs):
update_wrapper(view, cls.dispatch, assigned=())
return view
+ @classonlymethod
+ def as_instance(cls, request='', *args, **kwargs):
+ """
+ Creates an instance of the class, useful for isolated testing.
+ You can create a request through RequestFactory and pass args/kwargs as
+ you would do for a reverse() call.
+ """
+ view = cls()

The **initkwargs used in as_view() is missing, isn't it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@mjtamlyn
Collaborator

I quite like @benoitbryon's suggestion. Instead of implementing as_instance(), factor out as_view() to look like:

    @classonlymethod
    def as_view(cls, **initkwargs):
        # ... (unchanged)

        def view(request, *args, **kwargs):
            self = cls(**initkwargs).setup(request, *args, **kwargs)
            return self.dispatch(request, *args, **kwargs)

Then in your tests you can do cls(**initkwargs).setup(request, *args, **kwargs).

At the moment the current patch has a bit too much duplication for my liking.

@Keats

I updated the PR @benoitbryon @mjtamlyn
If you're ok with that way, I'll update the rest of the file

@mjtamlyn
Collaborator

Looks good to me. It will need documentation - in particular it might be worth documenting that you can pass extra arguments to the __init__ and they will get set on the view (if the view already has that attr), for example you can do:

view = MyDetailView(object=instance).setup(request, **kwargs)
view.get_context_data()

as SingleObjectMixin assumes that self.object is set before get_context_data().

django/views/generic/base.py
@@ -63,9 +63,7 @@ def view(request, *args, **kwargs):
self = cls(**initkwargs)
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get

Isn't it a part of the setup? What about moving it to setup() too?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Keats Keats Fixed #20456: easier unit-testing for CBV
Added a class method to return an instance of a CBV.
This allows to unit test part of a CBV directly, without using
the client.
48f9e7a
@Keats

Started changing detail and list tests, will do edit/dates another day

@Keats

I did completely forget about that PR, let me know if the current changes are ok and I'll finish it

@timgraham
Owner

I think quite a bit more documentation is needed as Marc suggested. It would be nice to have a small commit that has the basic implementation with some tests and then make refactoring the existing tests a separate commit. See also our patch review checklist.

@mjtamlyn mjtamlyn self-assigned this
@timgraham
Owner

Closing in absence of follow-up, feel free to send a new one if you want to continue working on this, thanks!

@timgraham timgraham closed this
@grahamu grahamu referenced this pull request in revsys/django-test-plus
Merged

Adding test methods and utilities for class-based views #11

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 13, 2014
  1. @Keats

    Fixed #20456: easier unit-testing for CBV

    Keats authored
    Added a class method to return an instance of a CBV.
    This allows to unit test part of a CBV directly, without using
    the client.
This page is out of date. Refresh to see the latest.
View
20 django/views/generic/base.py
@@ -61,11 +61,7 @@ def as_view(cls, **initkwargs):
def view(request, *args, **kwargs):
self = cls(**initkwargs)
- if hasattr(self, 'get') and not hasattr(self, 'head'):
- self.head = self.get
- self.request = request
- self.args = args
- self.kwargs = kwargs
+ self.setup(request, *args, **kwargs)
return self.dispatch(request, *args, **kwargs)
# take name and docstring from class
@@ -76,6 +72,20 @@ def view(request, *args, **kwargs):
update_wrapper(view, cls.dispatch, assigned=())
return view
+ def setup(self, request=None, *args, **kwargs):
+ """
+ Sets attributes on self, useful for isolated testing without having
+ to configure URLs.
+ You can create a request through RequestFactory and pass args/kwargs as
+ you would do for a reverse() call.
+ """
+ if hasattr(self, 'get') and not hasattr(self, 'head'):
+ self.head = self.get
+ self.request = request
+ self.args = args
+ self.kwargs = kwargs
+ return self
+
def dispatch(self, request, *args, **kwargs):
# Try to dispatch to the right method; if a method doesn't exist,
# defer to the error handler. Also defer to the error handler if the
View
19 docs/topics/class-based-views/intro.txt
@@ -288,3 +288,22 @@ login protection.
as parameters to the decorated method on the class. If your method
does not accept a compatible set of parameters it will raise a
``TypeError`` exception.
+
+Unit-testing class-based views
+==============================
+
+Every class-based views possess a method called setup where you can pass
+optional parameters: request, args and kwargs and get the view as an instance
+back.
+For example::
+
+
+ class MyView(View):
+ def calculate(self):
+ return 42
+
+ # And in your tests
+
+ def test_calculate(self):
+ test_view = MyView().setup()
+ self.assertEqual(test_view.calculate(), 42)
View
21 tests/generic_views/test_base.py
@@ -294,11 +294,14 @@ def test_extra_template_params(self):
"""
A template view can be customized to return extra context.
"""
- response = self.client.get('/template/custom/bar/')
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context['foo'], 'bar')
- self.assertEqual(response.context['key'], 'value')
- self.assertIsInstance(response.context['view'], View)
+ request = RequestFactory().get('/dummy')
+ kwargs = {
+ 'foo': 'bar'
+ }
+ view = views.CustomTemplateView().setup(request, **kwargs)
+ context = view.get_context_data()
+ self.assertEqual(context['key'], 'value')
+ self.assertIsInstance(context['view'], View)
def test_cached_views(self):
"""
@@ -473,15 +476,15 @@ class UseMultipleObjectMixinTest(unittest.TestCase):
rf = RequestFactory()
def test_use_queryset_from_view(self):
- test_view = views.CustomMultipleObjectMixinView()
- test_view.get(self.rf.get('/'))
+ test_view = views.CustomMultipleObjectMixinView().setup()
+ test_view.object_list = test_view.get_queryset()
# Don't pass queryset as argument
context = test_view.get_context_data()
self.assertEqual(context['object_list'], test_view.queryset)
def test_overwrite_queryset(self):
- test_view = views.CustomMultipleObjectMixinView()
- test_view.get(self.rf.get('/'))
+ test_view = views.CustomMultipleObjectMixinView().setup()
+ test_view.object_list = test_view.get_queryset()
queryset = [{'name': 'Lennon'}, {'name': 'Ono'}]
self.assertNotEqual(test_view.queryset, queryset)
# Overwrite the view's queryset with queryset from kwarg
View
116 tests/generic_views/test_detail.py
@@ -1,100 +1,105 @@
from __future__ import unicode_literals
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
-from django.test import TestCase
+from django.http import Http404
+from django.test import TestCase, RequestFactory
from django.views.generic.base import View
from .models import Artist, Author, Page
+from . import views
class DetailViewTest(TestCase):
fixtures = ['generic-views-test-data.json']
urls = 'generic_views.urls'
+ rf = RequestFactory()
+
+ def _get_view_context(self, view_class, initkwargs=None, kwargs=None):
+ initkwargs = initkwargs or {}
+ kwargs = kwargs or {}
+ test_view = view_class(**initkwargs).setup(self.rf.get('/'), **kwargs)
+ test_view.object = test_view.get_object()
+
+ return test_view.get_context_data()
def test_simple_object(self):
- res = self.client.get('/detail/obj/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['object'], {'foo': 'bar'})
- self.assertIsInstance(res.context['view'], View)
- self.assertTemplateUsed(res, 'generic_views/detail.html')
+ context = self._get_view_context(views.ObjectDetail)
+ self.assertEqual(context['object'], {'foo': 'bar'})
+ self.assertIsInstance(context['view'], View)
def test_detail_by_pk(self):
- res = self.client.get('/detail/author/1/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['object'], Author.objects.get(pk=1))
- self.assertEqual(res.context['author'], Author.objects.get(pk=1))
- self.assertTemplateUsed(res, 'generic_views/author_detail.html')
+ context = self._get_view_context(views.AuthorDetail, None, {'pk': 1})
+ author = Author.objects.get(pk=1)
+ self.assertEqual(context['object'], author)
+ self.assertEqual(context['author'], author)
def test_detail_missing_object(self):
- res = self.client.get('/detail/author/500/')
- self.assertEqual(res.status_code, 404)
+ with self.assertRaises(Http404):
+ self._get_view_context(views.AuthorDetail, None, {'pk': 500})
def test_detail_object_does_not_exist(self):
- self.assertRaises(ObjectDoesNotExist, self.client.get, '/detail/doesnotexist/1/')
+ with self.assertRaises(ObjectDoesNotExist):
+ self._get_view_context(views.ObjectDoesNotExistDetail, None, {'pk': 1})
def test_detail_by_custom_pk(self):
- res = self.client.get('/detail/author/bycustompk/1/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['object'], Author.objects.get(pk=1))
- self.assertEqual(res.context['author'], Author.objects.get(pk=1))
- self.assertTemplateUsed(res, 'generic_views/author_detail.html')
+ context = self._get_view_context(views.AuthorDetail, {'pk_url_kwarg': 'foo'}, {'foo': 1})
+ author = Author.objects.get(pk=1)
+ self.assertEqual(context['object'], author)
+ self.assertEqual(context['author'], author)
def test_detail_by_slug(self):
- res = self.client.get('/detail/author/byslug/scott-rosenberg/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['object'], Author.objects.get(slug='scott-rosenberg'))
- self.assertEqual(res.context['author'], Author.objects.get(slug='scott-rosenberg'))
- self.assertTemplateUsed(res, 'generic_views/author_detail.html')
+ context = self._get_view_context(views.AuthorDetail, None, {'slug': 'scott-rosenberg'})
+ author = Author.objects.get(slug='scott-rosenberg')
+ self.assertEqual(context['object'], author)
+ self.assertEqual(context['author'], author)
def test_detail_by_custom_slug(self):
- res = self.client.get('/detail/author/bycustomslug/scott-rosenberg/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['object'], Author.objects.get(slug='scott-rosenberg'))
- self.assertEqual(res.context['author'], Author.objects.get(slug='scott-rosenberg'))
- self.assertTemplateUsed(res, 'generic_views/author_detail.html')
-
- def test_verbose_name(self):
- res = self.client.get('/detail/artist/1/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['object'], Artist.objects.get(pk=1))
- self.assertEqual(res.context['artist'], Artist.objects.get(pk=1))
- self.assertTemplateUsed(res, 'generic_views/artist_detail.html')
+ context = self._get_view_context(
+ views.AuthorDetail, {'slug_url_kwarg': 'foo'}, {'foo': 'scott-rosenberg'}
+ )
+ author = Author.objects.get(slug='scott-rosenberg')
+ self.assertEqual(context['object'], author)
+ self.assertEqual(context['author'], author)
def test_template_name(self):
res = self.client.get('/detail/author/1/template_name/')
self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['object'], Author.objects.get(pk=1))
- self.assertEqual(res.context['author'], Author.objects.get(pk=1))
+ author = Author.objects.get(pk=1)
+ self.assertEqual(res.context['object'], author)
+ self.assertEqual(res.context['author'], author)
self.assertTemplateUsed(res, 'generic_views/about.html')
def test_template_name_suffix(self):
res = self.client.get('/detail/author/1/template_name_suffix/')
self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['object'], Author.objects.get(pk=1))
- self.assertEqual(res.context['author'], Author.objects.get(pk=1))
+ author = Author.objects.get(pk=1)
+ self.assertEqual(res.context['object'], author)
+ self.assertEqual(res.context['author'], author)
self.assertTemplateUsed(res, 'generic_views/author_view.html')
def test_template_name_field(self):
res = self.client.get('/detail/page/1/field/')
self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['object'], Page.objects.get(pk=1))
- self.assertEqual(res.context['page'], Page.objects.get(pk=1))
+ page = Page.objects.get(pk=1)
+ self.assertEqual(res.context['object'], page)
+ self.assertEqual(res.context['page'], page)
self.assertTemplateUsed(res, 'generic_views/page_template.html')
def test_context_object_name(self):
- res = self.client.get('/detail/author/1/context_object_name/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['object'], Author.objects.get(pk=1))
- self.assertEqual(res.context['thingy'], Author.objects.get(pk=1))
- self.assertFalse('author' in res.context)
- self.assertTemplateUsed(res, 'generic_views/author_detail.html')
+ context = self._get_view_context(
+ views.AuthorDetail, {'context_object_name': 'thingy'}, {'pk': 1}
+ )
+ author = Author.objects.get(pk=1)
+ self.assertEqual(context['object'], author)
+ self.assertEqual(context['thingy'], author)
+ self.assertFalse('author' in context)
def test_duplicated_context_object_name(self):
- res = self.client.get('/detail/author/1/dupe_context_object_name/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['object'], Author.objects.get(pk=1))
- self.assertFalse('author' in res.context)
- self.assertTemplateUsed(res, 'generic_views/author_detail.html')
+ context = self._get_view_context(
+ views.AuthorDetail, {'context_object_name': 'object'}, {'pk': 1}
+ )
+ self.assertEqual(context['object'], Author.objects.get(pk=1))
+ self.assertFalse('author' in context)
def test_invalid_url(self):
self.assertRaises(AttributeError, self.client.get, '/detail/author/invalid/url/')
@@ -103,6 +108,5 @@ def test_invalid_queryset(self):
self.assertRaises(ImproperlyConfigured, self.client.get, '/detail/author/invalid/qs/')
def test_non_model_object_with_meta(self):
- res = self.client.get('/detail/nonmodel/1/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['object'].id, "non_model_1")
+ context = self._get_view_context(views.NonModelDetail, None, {'pk': 1})
+ self.assertEqual(context['object'].id, "non_model_1")
View
217 tests/generic_views/test_list.py
@@ -1,76 +1,81 @@
from __future__ import unicode_literals
from django.core.exceptions import ImproperlyConfigured
-from django.test import TestCase, override_settings
+from django.http import Http404
+from django.test import TestCase, override_settings, RequestFactory
from django.views.generic.base import View
from django.utils.encoding import force_str
from .models import Author, Artist
+from . import views
class ListViewTests(TestCase):
fixtures = ['generic-views-test-data.json']
urls = 'generic_views.urls'
+ rf = RequestFactory()
+
+ def _get_view_context(self, view_class, initkwargs=None, kwargs=None):
+ initkwargs = initkwargs or {}
+ kwargs = kwargs or {}
+ test_view = view_class(**initkwargs).setup(self.rf.get('/'), **kwargs)
+ test_view.object_list = test_view.get_queryset()
+
+ return test_view.get_context_data()
def test_items(self):
- res = self.client.get('/list/dict/')
- self.assertEqual(res.status_code, 200)
- self.assertTemplateUsed(res, 'generic_views/list.html')
- self.assertEqual(res.context['object_list'][0]['first'], 'John')
+ context = self._get_view_context(views.DictList)
+ self.assertEqual(context['object_list'][0]['first'], 'John')
def test_queryset(self):
- res = self.client.get('/list/authors/')
- self.assertEqual(res.status_code, 200)
- self.assertTemplateUsed(res, 'generic_views/author_list.html')
- self.assertEqual(list(res.context['object_list']), list(Author.objects.all()))
- self.assertIsInstance(res.context['view'], View)
- self.assertIs(res.context['author_list'], res.context['object_list'])
- self.assertIsNone(res.context['paginator'])
- self.assertIsNone(res.context['page_obj'])
- self.assertFalse(res.context['is_paginated'])
+ context = self._get_view_context(views.AuthorList)
+
+ self.assertEqual(list(context['object_list']), list(Author.objects.all()))
+ self.assertIsInstance(context['view'], View)
+ self.assertIs(context['author_list'], context['object_list'])
+ self.assertIsNone(context['paginator'])
+ self.assertIsNone(context['page_obj'])
+ self.assertFalse(context['is_paginated'])
def test_paginated_queryset(self):
self._make_authors(100)
- res = self.client.get('/list/authors/paginated/')
- self.assertEqual(res.status_code, 200)
- self.assertTemplateUsed(res, 'generic_views/author_list.html')
- self.assertEqual(len(res.context['object_list']), 30)
- self.assertIs(res.context['author_list'], res.context['object_list'])
- self.assertTrue(res.context['is_paginated'])
- self.assertEqual(res.context['page_obj'].number, 1)
- self.assertEqual(res.context['paginator'].num_pages, 4)
- self.assertEqual(res.context['author_list'][0].name, 'Author 00')
- self.assertEqual(list(res.context['author_list'])[-1].name, 'Author 29')
+ context = self._get_view_context(views.AuthorList, {'paginate_by': 30})
+
+ self.assertEqual(len(context['object_list']), 30)
+ self.assertIs(context['author_list'], context['object_list'])
+ self.assertTrue(context['is_paginated'])
+ self.assertEqual(context['page_obj'].number, 1)
+ self.assertEqual(context['paginator'].num_pages, 4)
+ self.assertEqual(context['author_list'][0].name, 'Author 00')
+ self.assertEqual(list(context['author_list'])[-1].name, 'Author 29')
def test_paginated_queryset_shortdata(self):
# Test that short datasets ALSO result in a paginated view.
- res = self.client.get('/list/authors/paginated/')
- self.assertEqual(res.status_code, 200)
- self.assertTemplateUsed(res, 'generic_views/author_list.html')
- self.assertEqual(list(res.context['object_list']), list(Author.objects.all()))
- self.assertIs(res.context['author_list'], res.context['object_list'])
- self.assertEqual(res.context['page_obj'].number, 1)
- self.assertEqual(res.context['paginator'].num_pages, 1)
- self.assertFalse(res.context['is_paginated'])
+ context = self._get_view_context(views.AuthorList, {'paginate_by': 30})
- def test_paginated_get_page_by_query_string(self):
+ self.assertEqual(list(context['object_list']), list(Author.objects.all()))
+ self.assertIs(context['author_list'], context['object_list'])
+ self.assertEqual(context['page_obj'].number, 1)
+ self.assertEqual(context['paginator'].num_pages, 1)
+ self.assertFalse(context['is_paginated'])
+
+ def test_paginated_get_page(self):
self._make_authors(100)
- res = self.client.get('/list/authors/paginated/', {'page': '2'})
- self.assertEqual(res.status_code, 200)
- self.assertTemplateUsed(res, 'generic_views/author_list.html')
- self.assertEqual(len(res.context['object_list']), 30)
- self.assertIs(res.context['author_list'], res.context['object_list'])
- self.assertEqual(res.context['author_list'][0].name, 'Author 30')
- self.assertEqual(res.context['page_obj'].number, 2)
+ context = self._get_view_context(views.AuthorList, {'paginate_by': 30}, {'page': 2})
- def test_paginated_get_last_page_by_query_string(self):
+ self.assertEqual(len(context['object_list']), 30)
+ self.assertIs(context['author_list'], context['object_list'])
+ self.assertEqual(context['author_list'][0].name, 'Author 30')
+ self.assertEqual(context['page_obj'].number, 2)
+
+ def test_paginated_get_last_page(self):
self._make_authors(100)
- res = self.client.get('/list/authors/paginated/', {'page': 'last'})
- self.assertEqual(res.status_code, 200)
- self.assertEqual(len(res.context['object_list']), 10)
- self.assertIs(res.context['author_list'], res.context['object_list'])
- self.assertEqual(res.context['author_list'][0].name, 'Author 90')
- self.assertEqual(res.context['page_obj'].number, 4)
+ context = self._get_view_context(views.AuthorList, {'paginate_by': 30}, {'page': 'last'})
+
+ self.assertEqual(len(context['object_list']), 10)
+ self.assertIs(context['author_list'], context['object_list'])
+ self.assertEqual(context['author_list'][0].name, 'Author 90')
+ self.assertEqual(context['page_obj'].number, 4)
def test_paginated_get_page_by_urlvar(self):
self._make_authors(100)
@@ -82,6 +87,16 @@ def test_paginated_get_page_by_urlvar(self):
self.assertEqual(res.context['author_list'][0].name, 'Author 60')
self.assertEqual(res.context['page_obj'].number, 3)
+ def test_paginated_get_page_by_query_string(self):
+ self._make_authors(100)
+ res = self.client.get('/list/authors/paginated/', {'page': '3'})
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/author_list.html')
+ self.assertEqual(len(res.context['object_list']), 30)
+ self.assertIs(res.context['author_list'], res.context['object_list'])
+ self.assertEqual(res.context['author_list'][0].name, 'Author 60')
+ self.assertEqual(res.context['page_obj'].number, 3)
+
def test_paginated_page_out_of_range(self):
self._make_authors(100)
res = self.client.get('/list/authors/paginated/42/')
@@ -94,61 +109,66 @@ def test_paginated_invalid_page(self):
def test_paginated_custom_paginator_class(self):
self._make_authors(7)
- res = self.client.get('/list/authors/paginated/custom_class/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['paginator'].num_pages, 1)
+ initkwargs = {
+ 'paginate_by': 5,
+ 'paginator_class': views.CustomPaginator
+ }
+ context = self._get_view_context(views.AuthorList, initkwargs)
+
+ self.assertEqual(context['paginator'].num_pages, 1)
# Custom pagination allows for 2 orphans on a page size of 5
- self.assertEqual(len(res.context['object_list']), 7)
+ self.assertEqual(len(context['object_list']), 7)
def test_paginated_custom_page_kwarg(self):
self._make_authors(100)
- res = self.client.get('/list/authors/paginated/custom_page_kwarg/', {'pagina': '2'})
- self.assertEqual(res.status_code, 200)
- self.assertTemplateUsed(res, 'generic_views/author_list.html')
- self.assertEqual(len(res.context['object_list']), 30)
- self.assertIs(res.context['author_list'], res.context['object_list'])
- self.assertEqual(res.context['author_list'][0].name, 'Author 30')
- self.assertEqual(res.context['page_obj'].number, 2)
+ initkwargs = {
+ 'paginate_by': 30,
+ 'page_kwarg': 'pagina'
+ }
+ context = self._get_view_context(views.AuthorList, initkwargs, {'pagina': 2})
+
+ self.assertEqual(len(context['object_list']), 30)
+ self.assertIs(context['author_list'], context['object_list'])
+ self.assertEqual(context['author_list'][0].name, 'Author 30')
+ self.assertEqual(context['page_obj'].number, 2)
def test_paginated_custom_paginator_constructor(self):
self._make_authors(7)
- res = self.client.get('/list/authors/paginated/custom_constructor/')
- self.assertEqual(res.status_code, 200)
+ context = self._get_view_context(views.AuthorListCustomPaginator)
+
# Custom pagination allows for 2 orphans on a page size of 5
- self.assertEqual(len(res.context['object_list']), 7)
+ self.assertEqual(len(context['object_list']), 7)
def test_paginated_orphaned_queryset(self):
self._make_authors(92)
- res = self.client.get('/list/authors/paginated-orphaned/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['page_obj'].number, 1)
- res = self.client.get(
- '/list/authors/paginated-orphaned/', {'page': 'last'})
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['page_obj'].number, 3)
- res = self.client.get(
- '/list/authors/paginated-orphaned/', {'page': '3'})
- self.assertEqual(res.status_code, 200)
- self.assertEqual(res.context['page_obj'].number, 3)
- res = self.client.get(
- '/list/authors/paginated-orphaned/', {'page': '4'})
- self.assertEqual(res.status_code, 404)
+ initkwargs = {
+ 'paginate_by': 30,
+ 'paginate_orphans': 2
+ }
+ context = self._get_view_context(views.AuthorList, initkwargs)
+ self.assertEqual(context['page_obj'].number, 1)
- def test_paginated_non_queryset(self):
- res = self.client.get('/list/dict/paginated/')
+ context = self._get_view_context(views.AuthorList, initkwargs, {'page': 'last'})
+ self.assertEqual(context['page_obj'].number, 3)
- self.assertEqual(res.status_code, 200)
- self.assertEqual(len(res.context['object_list']), 1)
+ context = self._get_view_context(views.AuthorList, initkwargs, {'page': '3'})
+ self.assertEqual(context['page_obj'].number, 3)
+
+ with self.assertRaises(Http404):
+ self._get_view_context(views.AuthorList, initkwargs, {'page': '4'})
+
+ def test_paginated_non_queryset(self):
+ context = self._get_view_context(views.DictList, {'paginate_by': 1})
+ self.assertEqual(len(context['object_list']), 1)
def test_verbose_name(self):
- res = self.client.get('/list/artists/')
- self.assertEqual(res.status_code, 200)
- self.assertTemplateUsed(res, 'generic_views/list.html')
- self.assertEqual(list(res.context['object_list']), list(Artist.objects.all()))
- self.assertIs(res.context['artist_list'], res.context['object_list'])
- self.assertIsNone(res.context['paginator'])
- self.assertIsNone(res.context['page_obj'])
- self.assertFalse(res.context['is_paginated'])
+ context = self._get_view_context(views.ArtistList)
+
+ self.assertEqual(list(context['object_list']), list(Artist.objects.all()))
+ self.assertIs(context['artist_list'], context['object_list'])
+ self.assertIsNone(context['paginator'])
+ self.assertIsNone(context['page_obj'])
+ self.assertFalse(context['is_paginated'])
def test_allow_empty_false(self):
res = self.client.get('/list/authors/notempty/')
@@ -172,23 +192,20 @@ def test_template_name_suffix(self):
self.assertTemplateUsed(res, 'generic_views/author_objects.html')
def test_context_object_name(self):
- res = self.client.get('/list/authors/context_object_name/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(list(res.context['object_list']), list(Author.objects.all()))
- self.assertNotIn('authors', res.context)
- self.assertIs(res.context['author_list'], res.context['object_list'])
- self.assertTemplateUsed(res, 'generic_views/author_list.html')
+ context = self._get_view_context(views.AuthorList, {'context_object_name': 'author_list'})
+ self.assertEqual(list(context['object_list']), list(Author.objects.all()))
+ self.assertNotIn('authors', context)
+ self.assertIs(context['author_list'], context['object_list'])
def test_duplicate_context_object_name(self):
- res = self.client.get('/list/authors/dupe_context_object_name/')
- self.assertEqual(res.status_code, 200)
- self.assertEqual(list(res.context['object_list']), list(Author.objects.all()))
- self.assertNotIn('authors', res.context)
- self.assertNotIn('author_list', res.context)
- self.assertTemplateUsed(res, 'generic_views/author_list.html')
+ context = self._get_view_context(views.AuthorList, {'context_object_name': 'object_list'})
+ self.assertEqual(list(context['object_list']), list(Author.objects.all()))
+ self.assertNotIn('authors', context)
+ self.assertNotIn('author_list', context)
def test_missing_items(self):
- self.assertRaises(ImproperlyConfigured, self.client.get, '/list/authors/invalid/')
+ with self.assertRaises(ImproperlyConfigured):
+ self._get_view_context(views.AuthorList, {'queryset': None})
def test_paginated_list_view_does_not_load_entire_table(self):
# Regression test for #17535
View
42 tests/generic_views/urls.py
@@ -16,8 +16,6 @@
TemplateView.as_view()),
(r'^template/simple/(?P<foo>\w+)/$',
TemplateView.as_view(template_name='generic_views/about.html')),
- (r'^template/custom/(?P<foo>\w+)/$',
- views.CustomTemplateView.as_view(template_name='generic_views/about.html')),

This is one thing I like with this technique: we do not need to create and maintain URLconfs just for test purpose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
(r'^template/content_type/$',
TemplateView.as_view(template_name='generic_views/robots.txt', content_type='text/plain')),
@@ -25,38 +23,23 @@
cache_page(2.0)(TemplateView.as_view(template_name='generic_views/about.html'))),
# DetailView
- (r'^detail/obj/$',
- views.ObjectDetail.as_view()),
url(r'^detail/artist/(?P<pk>\d+)/$',
views.ArtistDetail.as_view(),
name="artist_detail"),
url(r'^detail/author/(?P<pk>\d+)/$',
views.AuthorDetail.as_view(),
name="author_detail"),
- (r'^detail/author/bycustompk/(?P<foo>\d+)/$',
- views.AuthorDetail.as_view(pk_url_kwarg='foo')),
- (r'^detail/author/byslug/(?P<slug>[\w-]+)/$',
- views.AuthorDetail.as_view()),
- (r'^detail/author/bycustomslug/(?P<foo>[\w-]+)/$',
- views.AuthorDetail.as_view(slug_url_kwarg='foo')),
(r'^detail/author/(?P<pk>\d+)/template_name_suffix/$',
views.AuthorDetail.as_view(template_name_suffix='_view')),
(r'^detail/author/(?P<pk>\d+)/template_name/$',
views.AuthorDetail.as_view(template_name='generic_views/about.html')),
- (r'^detail/author/(?P<pk>\d+)/context_object_name/$',
- views.AuthorDetail.as_view(context_object_name='thingy')),
- (r'^detail/author/(?P<pk>\d+)/dupe_context_object_name/$',
- views.AuthorDetail.as_view(context_object_name='object')),
(r'^detail/page/(?P<pk>\d+)/field/$',
views.PageDetail.as_view()),
(r'^detail/author/invalid/url/$',
views.AuthorDetail.as_view()),
(r'^detail/author/invalid/qs/$',
views.AuthorDetail.as_view(queryset=None)),
- (r'^detail/nonmodel/1/$',
- views.NonModelDetail.as_view()),
- (r'^detail/doesnotexist/(?P<pk>\d+)/$',
- views.ObjectDoesNotExistDetail.as_view()),
+
# FormView
(r'^contact/$',
views.ContactView.as_view()),
@@ -126,22 +109,13 @@
views.BookSigningArchive.as_view()),
# ListView
- (r'^list/dict/$',
- views.DictList.as_view()),
- (r'^list/dict/paginated/$',
- views.DictList.as_view(paginate_by=1)),
- url(r'^list/artists/$',
- views.ArtistList.as_view(),
- name="artists_list"),
url(r'^list/authors/$',
views.AuthorList.as_view(),
- name="authors_list"),
+ name='authors_list'),
(r'^list/authors/paginated/$',
views.AuthorList.as_view(paginate_by=30)),
(r'^list/authors/paginated/(?P<page>\d+)/$',
views.AuthorList.as_view(paginate_by=30)),
- (r'^list/authors/paginated-orphaned/$',
- views.AuthorList.as_view(paginate_by=30, paginate_orphans=2)),
(r'^list/authors/notempty/$',
views.AuthorList.as_view(allow_empty=False)),
(r'^list/authors/notempty/paginated/$',
@@ -150,18 +124,6 @@
views.AuthorList.as_view(template_name='generic_views/list.html')),
(r'^list/authors/template_name_suffix/$',
views.AuthorList.as_view(template_name_suffix='_objects')),
- (r'^list/authors/context_object_name/$',
- views.AuthorList.as_view(context_object_name='author_list')),
- (r'^list/authors/dupe_context_object_name/$',
- views.AuthorList.as_view(context_object_name='object_list')),
- (r'^list/authors/invalid/$',
- views.AuthorList.as_view(queryset=None)),
- (r'^list/authors/paginated/custom_class/$',
- views.AuthorList.as_view(paginate_by=5, paginator_class=views.CustomPaginator)),
- (r'^list/authors/paginated/custom_page_kwarg/$',
- views.AuthorList.as_view(paginate_by=30, page_kwarg='pagina')),
- (r'^list/authors/paginated/custom_constructor/$',
- views.AuthorListCustomPaginator.as_view()),
# YearArchiveView
# Mixing keyword and possitional captures below is intentional; the views
View
3  tests/generic_views/views.py
@@ -225,9 +225,6 @@ class CustomMultipleObjectMixinView(generic.list.MultipleObjectMixin, generic.Vi
{'name': 'Yoko'},
]
- def get(self, request):
- self.object_list = self.get_queryset()
-
class CustomContextView(generic.detail.SingleObjectMixin, generic.View):
model = Book
Something went wrong with that request. Please try again.