14

This works

def test_access_to_home_with_location(self):
    self.client.login(username=self.user.get_username(), password='pass')
    session = self.client.session
    session['location'] = [42]
    session.save()
    response = self.client.get(reverse('home'))

But this

def test_access_to_home_with_location(self):
    session = self.client.session
    session['location'] = [42]
    session.save()
    response = self.client.get(reverse('home'))

breaks with

====================================================================== 
ERROR: test_access_to_home_with_location (posts.tests.HomeViewTestCase)       
----------------------------------------------------------------------      
Traceback (most recent call last):                                            
  File "tests.py", line 32, in test_access_to_home_with_location                            
    session.save()                                                              
AttributeError: 'dict' object has no attribute 'save'

So it seems with out calling self.client.login() self.client.session is just an empty dictionary. Is there a way to initialize it as a session object?

Ben
  • 6,986
  • 6
  • 44
  • 71

4 Answers4

15

Please note that workarounds are no longer necessary. The original question's snippet which did not work should now work :

session = self.client.session
session['location'] = [42]
session.save()

https://docs.djangoproject.com/en/2.0/topics/testing/tools/#django.test.Client.session

Brachamul
  • 1,886
  • 2
  • 21
  • 34
  • 2
    I believe it's fixed in the [following](https://code.djangoproject.com/ticket/21357) [commit](https://github.com/django/django/commit/be88b062afaa58559bb12623e8ed8843f07b97a1#diff-97160f50594424a40f2621d5a3c581ccR396). Make sure you put session into a variable before making changes like in the answer, since every time you write `self.client.session` you get [new object](https://github.com/django/django/blob/2.2/django/test/client.py#L452-L463). – x-yuri Apr 06 '19 at 09:43
  • @x-yuri could you write up an example so I can replace my answer ? – Brachamul Apr 06 '19 at 13:26
  • Your example is fine, I just added some details. – x-yuri Apr 06 '19 at 13:30
7

When no cookies are set in the client the session property is a empty dict, hence your error. Here is the relevant source of django.test.client.Client:

def _session(self):
    """
    Obtains the current session variables.
    """
    if 'django.contrib.sessions' in settings.INSTALLED_APPS:
        engine = import_module(settings.SESSION_ENGINE)
        cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
        if cookie:
            return engine.SessionStore(cookie.value)
    return {}
session = property(_session)

Since you are not logged in the cookie with the key matching settings.SESSION_COOKIE_NAME is not found.

However you could manually create a session object like this:

if not self.client.session:
    engine = import_module(settings.SESSION_ENGINE)

    self.client.session = engine.SessionStore()
    self.client.session.save()

This is the way the login handler in Client creates a new session.

EDIT: I realized you also need to save the session key in a cookie so that the next request uses the same session

Here's a helper function you could put in your Client sub class that creates a new session and a referencing cookie:

def set_session_data(self, key, value):
    """Shortcut for setting session data regardless of being authenticated"""

    if not self.client.session:
        # Save new session in database and add cookie referencing it

        engine = import_module(settings.SESSION_ENGINE)

        self.client.session = engine.SessionStore()
        self.client.session.save()

        session_cookie = settings.SESSION_COOKIE_NAME
        self.client.cookies[session_cookie] = self.client.session.session_key
        cookie_data = {
            'max-age': None,
            'path': '/',
            'domain': settings.SESSION_COOKIE_DOMAIN,
            'secure': settings.SESSION_COOKIE_SECURE or None,
            'expires': None,
        }
        self.client.cookies[session_cookie].update(cookie_data)

    self.client.session[key] = value
    self.client.session.save()

Note: I'm not saying this is the only way to do this, this is one way I found out by reading the django source code. The code in this answer is not tested/run, therefor it might need some fine tuning.

Further reading

To read about how SessionStore works you can look at the django.contrib.sessions module.

To read about how session and cookies are handled in Client you can look at django.test.client.Client.

rzetterberg
  • 10,146
  • 4
  • 44
  • 54
  • 2
    I'm surprised this is such an involved task. It seems like something that would be commonly needed. Is there a better approach for doing this kind of testing? – Ben Aug 05 '14 at 20:33
  • @Ben I don't see why you would ever need to set a cookie value inside a test, to be honest. What is it you are trying to achieve? – rzetterberg Aug 05 '14 at 22:37
  • I have several views where the information displayed is dependent on the location that a user selects. As a very rough example: user is interested in the Los Angeles area. They could pull up a view showing lunch spots. Then without reselecting their location, they could pull up a different view for bars. I figured the best way was to pass their location around in a session variable. I wanted to write some tests to make sure certain functionality was working when a location was selected and when one wasn't. I'm still new to web development, is this the right way to go about it? – Ben Aug 05 '14 at 23:58
  • @Ben I see, that sounds reasonable. In my opinion it would be better to use a Selenium test which actually selects the location using the the form provided by the view. This way your test will be closer to how an actual user interacts with your website. You can read more about Selenium here: https://pypi.python.org/pypi/selenium – rzetterberg Aug 06 '14 at 07:25
  • 1
    If a client session already exists, you'll need to use `session = self.client.session\n session[key] = value\n session.save()` The direct reference to `self.client.session[key] = value` wouldn't work for me - also mentioned in comments [here](http://stackoverflow.com/a/7503465/3211687) – Conor Svensson Nov 24 '15 at 21:46
  • It doesn't matter whether the session exists or not, you've got to put `self.client.session` into a variable, do the changes, then `save()`. Make sure not to change `self.client.session` directly, since every time you write this piece of code, you get [new object](https://github.com/django/django/blob/2.2/django/test/client.py#L452-L463). More up-to-date [answer](https://stackoverflow.com/a/49837833/52499). @Ben care to accept? – x-yuri Apr 06 '19 at 09:51
3

rzetterberg answer is the more rigorous one so I think it should remain accepted, but this way looks like it will also work

def setUp(self):
    """
    set up sessions for anonymous users
    """
    engine = import_module(settings.SESSION_ENGINE)
    store = engine.SessionStore()
    store.save()  
    self.client.cookies[settings.SESSION_COOKIE_NAME] = store.session_key

It looks like there is a current ticket open on this topic (started 5 years ago... but active within the last few months):
https://code.djangoproject.com/ticket/10899
and
https://code.djangoproject.com/ticket/11475

Ben
  • 6,986
  • 6
  • 44
  • 71
1

I used RequestFactory to generate a request object, set the session values manually, and passed it to the view function, as laid out in the docs.

from django.test import TestCase, RequestFactory
from django.urls import reverse

from .views import test_view

class MyTestCase(TestCase):
    def setUp(self):
        self.factory = RequestFactory()

    def test_view_x(self):
        request = self.factory.get(reverse('myapp:test_view'))
        request.session = {'foo': 'bar'}

        response = test_view(request)

        ...

        # Run some test on the response to ensure the view is returning
        # the expected value

Using RequestFactory to generate a request object lets you test a view function as you would test any other function - treating it as a black box, passing certain inputs and checking for the correct output. Note that you're running the request in isolation, without any of the installed middleware.

Jonathan Cox
  • 341
  • 1
  • 7
  • 14