Testing ListView classes

I’ve been trying to fill some testing gaps and have started looking at testing views. I’m trying to do some true unit testing where I’m testing isolated components and I’m not clear about the best way to test for example, the get_context_data method of a ListView class.

I ended up doing this:

    def test_get_context_data(self):
        # This creates a GET request.  The URL argument doesn't matter.  We just want the request object, with a little
        # bit of setup.
        request = RequestFactory().get("/")

        # Add the cookie we want.
        request.COOKIES = {"StudyLV-filter-name": "2"}

        # The cookies are handled in the constructor.
        slv = StudyLV(request=request)  # This is my ListView class

        # To test individual methods in Django's class based views, there can be some things you have to setup manually?
        slv.object_list = slv.get_queryset()[:]

        # This is the method we are testing.
        context = slv.get_context_data()

        # There are 2 study records possible to retrieve.
        self.assertEqual(2, context["raw_total"])

        # The cookie said to search for studies whose name contains "2".  There's only 1 of those.
        self.assertEqual(1, context["total"])

What I don’t quite understand is why I had to set slv.object_list manually. If I didn’t, I got:

...
  File "/Users/rleach/PROJECT-LOCAL/TRACEBASE/tracebase/.venv/lib/python3.9/site-packages/django/views/generic/list.py", line 124, in get_context_data
    queryset = object_list if object_list is not None else self.object_list
AttributeError: 'StudyLV' object has no attribute 'object_list'

I’m implementing this test on new code. It’s based on a rapid prototype that works, but I have made some changes, so perhaps the need to set object_list manually is due to something I did? Regardless, I’d like to understand the order in which the builtin methods are called.

I extended the following methods:

  • __init__
  • get_queryset
  • paginate_queryset
  • get_paginate_by
  • get_context_data

I thought I would just need to create the object with the request and call get_context_data(). I tried following the Django testing topics doc. It suggests I call slv.setup(request), but I wanted the cookies dealt with early on, to get them out of the way, so I handled them in the constructor. I’m not sure whether that’s the best idea or not, but either way, I tried with and without the call to setup and still got the same exception.

Any words of wisdom on this?

The object_list property of the view class is set in the get method - see ListView -- Classy CBV

The two sites I always direct people to for understanding the Django-provided GCBVs is the Classy Class-Based View site and the CBV diagrams page. I have found them to be extremely helpful.

Also, you can see on that CCBV page that get_context_data can accept object_list as a keyword parameter. This means you could do something like:
context = slv.get_context_data(object_list=slv.get_queryset())

Or, if what you’re testing doesn’t need pagination or the data from object_list, you could probably pass an empty list:
context=slv.get_context_data(object_list=[])

1 Like

I definitely have tests that set slv.object_list=[]. :slight_smile:

Since you mention pagination in that context, I’m thinking more about my cookie handling… There are a number of cookies outside of pagination that I’m using, such as filtering based on column value or searching (any column match), sorting, saved column visibility, export options, rows per page, soft-wrapping, etc.

I initially was just grabbing the cookies from the request object wherever they were needed, but when I ended up with a couple repeated retrievals, I decided I wanted one consolidated place to do all the cookie retrievals, as all of my ListView classes have a common Bootstrap Table interface. My idea was to do that in the constructor and set instance attributes in an inserted an intermediate superclass that I could re-use for all my ListView classes.

Thinking about it more, and thinking about my experience with mod_perl decades ago now, I’m not confident that doing it there is appropriate for any server, so perhaps instead of the constructor, I should be doing it in an extension of the get method you referred me to? What do you think?

One thing to keep in mind when working with Django CBVs is that a new instance of the class is created with every request. The as_view method creates the instance of the class when the url dispatcher calls it.

So what you’re doing with your “cookie retrieval” code is a “code organization” benefit, not a functional benefit. (It’s purely a personal decision as to whether that’s a pattern you wish to use.)

Also remember that the HttpRequest object is a Python object. Accessing things like cookies from within it is no more “effort” than retrieving any other attribute from any other object. (The real work is done when the HttpRequest object is created from the original request.) I would not worry about “repeated retrievals.”

Personally, if I were to ever store data in cookies, I would access that data on an as-needed basis. (But we don’t do that - all “data” is stored on the server. The only data we keep in a cookie is the session id. We don’t want people tampering with session-related information.)

My session experience is definitely very dated and I’d like to restart that learning curve, but currently we don’t have user accounts. (I think you need that for sessions?) Accounts may come in the future though, because some researchers have been reluctant to upload data to our site, even on our internal instance.

(We do CAS authenticate internally though…)

No you do not. Sessions are an independent construct, and can be used with the AnonymousUser object. (In fact, the dependency effectively works in the other direction. You need sessions - or something like them - in order to support the association of a user with a request.)

The identification of a session with a browser is by a cookie. The cookie no longer contains data - it holds a session id. The server stores the data within the server, identified by that id.

1 Like

Adding that to the todo list!

(As a personal side note, you might get a chuckle from the fact that I still have a CGI / Perl application in production that I support. It’s more than 25 years old now…)

I just googled for my old ones that had been taken over and hosted by NorthWestern for decades, but it seems they’ve evaporated, though we have an actively developed LIMS system that is 99% perl. I don’t work on it, but I do a PR review once in awhile. Like my old websites, my perl knowledge is slowly evaporating as well, lol.

I got to a concrete list view this week. While all my unit tests along the way were passing, once I started manually testing a real page, I ran into a problem that my code “organization” strategy didn’t account for. I have 3 levels of inheritance between ListView and a concrete ListView. Ignoring my poor class naming choices, it goes ListView > BSTClientInterface > BSTBaseListView > BSTListView > (e.g.) SampleListView. The separation of responsibilities is:

  • BSTClientInterface handles the cookies and JavaScript and URL parameters.
  • BSTBaseListView handles the column initialization based on the model and handles the initial sorting and filtering selections the user made via the cookies
  • BSTListView handles the querying

The first problem was that Django’s core code which creates the listview instances doesn’t pass the request object to the constructors, so none of my cookies were getting set.

So I tried moving all that logic into extensions of the get methods at each level. Then I ran into the second problem. All of that manipulation at each level has to be done before get_queryset is called and the BSTClientInterface has to run first, then BSTBaseListView, then BSTListView. But, ListView.get triggers the get_queryset call before that all returns.

So I solved that by creating separate methods and only extending the get method in BSTListView to grab and set the request and calling that other stack of methods.

It somewhat defeats my original code organization efforts, but at least it’s all obfuscated from the derived classes. SMH

I should have taken a cue from what you’d said about your standard practice of grabbing the cookie values when needed, but I wanted to be able to swap out the different levels of the hierarchy in the future. I suppose I could still make that work.

For now, what I’ve got works, but I thought I would ask:

Is it possible to tell that Django core code to pass the request object to the view constructors? If I knew how to do that, it would have saved me a day’s worth of work yesterday, sigh.

It is possible? Of course. Is it “easy”, or something that Django is designed to do? No.

From what I can see by looking at django.views.generic.base.View.as_view, my guess is you’d need to replace the base View class. You could either monkey patch that method, or replace it with your own version but then you’d also need to change everything that inherits from it to inherit from your modified version. (At that point, you’d probably be better off just creating your own class-based view library.)