#Testing views in Django

56 messages ยท Page 1 of 1 (latest)

honest onyx
#

Hello,

I'm trying to make my first tests in Django (well, in dev to be honest) and I'm having a bit of an issue when testing other things than models.

For example, when I try to test views like this :

from django.test import TestCase, Client
from cinemas_app.models import Cinema

class ChangeCinemaViewTest(TestCase):
    def setUp(self):
        self.client = Client()
        self.cinema1 = Cinema.objects.create(name="Cinema A", address="123", phone="1234567890")
        self.cinema2 = Cinema.objects.create(name="Cinema B", address="456", phone="0987654321")

    def test_change_cinema_with_client(self):
        session = self.client.session
        session["selected_cinema"] = self.cinema1.id
        session.save()

        response = self.client.post("/change_cinema/", {"cinema": self.cinema2.id}, HTTP_REFERER="/home/")

        self.assertEqual(self.client.session["selected_cinema"], self.cinema2.id)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response["Location"], "/home/")

I get "FAILED cinemas_app/tests/test_cinema_views.py::ChangeCinemaViewTest::test_change_cinema_with_client - django.core.exceptions.ImproperlyConfigured: The SECRET_KEY setting must not be empty. "

I've tried to setup the env adding this :

import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cinephoria.settings")

import django
django.setup()

But it does not help. The secret key is not directly into settings. Settings load an env file with the secret key in it.

What am I missing please ?

jolly rampart
#

how are you running your tests?

honest onyx
#

Right now just using pytest -v command.

In my CI, like this :

test:
  stage: test
  before_script:
    - python3 -m venv venv
    - source venv/bin/activate
    - pip install -r requirements.txt
    - pip install pytest pytest-django pytest-cov
  script:
    - pytest --maxfail=1 --disable-warnings -q --cov=mon_app  # Mesure la couverture de tests
  variables:
    DATABASE_ENGINE: 'sqlite3'  # Utilisation de SQLite3 pour les tests
    DATABASE_NAME: 'cinephoria.db'
  allow_failure: false
jolly rampart
honest onyx
#

Everything in the introduction is already done. I have a pytest.ini file with "[pytest]
DJANGO_SETTINGS_MODULE = cinephoria.settings" in it

jolly rampart
#

What does your SECRET_KEY setting look like?

honest onyx
#

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")

then the secret key is in the env file at project root.

jolly rampart
#

Ah right, but that env file is hopefully not commited to your repo? But if it is, there is nothing in your CI workflow to read from it. You would be better off setting that environment variable within Github directly

honest onyx
#

It is not commited. But the pytest fails already in local dev where the file is available

#

And yeah, SECRET KEY could be made available with gitlab env variables

jolly rampart
#

are you reading the file into django in your settings? otherwise you could provide a default value to the get function call

honest onyx
#

๐Ÿซฃ

#

what ?

jolly rampart
#

for the default:

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "this is a default insecure key")
#

for reading the env file, you want a package like python-decouple

honest onyx
#

I'm a bit scared with insecure keys. Isn't there a risk that the default insecure key could be used in prod if .env is not found ?

#

And I don't need python-decouple if I already use and load django-environ no ?

# Initialiser django-environ
env = environ.Env()
environ.Env.read_env(env_file)
jolly rampart
honest onyx
#

๐Ÿ˜ญ

jolly rampart
#

I do notice that your using django environ, but not in the case of your secret key?

honest onyx
#

I do use environ

# Env choice
env_file = ".env.local" if os.path.exists(".env.local") else ".env"

# Initialiser django-environ
env = environ.Env()
environ.Env.read_env(env_file)

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(os.environ.get("DEBUG", default=0))

ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS","127.0.0.1").split(",")

I just realized my SECRET_KEY line was above the env = envrion.... line.

I moved it as shown above and now I seem to crash on something else. Technically, it's progress

#

quick side question :

urlpatterns = [
path('change-cinema/', views.change_cinema, name='change_cinema'),
]

Should url path use "-" or "_" as best practice ?

jolly rampart
#

That's personal preference.

honest onyx
#

Thanks !

jolly rampart
honest onyx
#

Ok I'm going further now :

import os
from django.test import TestCase, Client
from django.urls import reverse
from cinemas_app.models import Cinema

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cinephoria.settings")

import django
django.setup()

class ChangeCinemaViewTest(TestCase):
    def setUp(self):
        self.client = Client()
        self.cinema1 = Cinema.objects.create(name="Cinema A", address="123", phone="1234567890")
        self.cinema2 = Cinema.objects.create(name="Cinema B", address="456", phone="0987654321")

    def test_change_cinema_with_client(self):
        session = self.client.session
        session["selected_cinema"] = self.cinema1.id
        session.save()

        response = self.client.post(reverse("change_cinema"), {"cinema": self.cinema2.id}, HTTP_REFERER="home")

        self.assertEqual(self.client.session["selected_cinema"], self.cinema2.id)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response["Location"], "home")
self = <test_cinemas_views.ChangeCinemaViewTest testMethod=test_change_cinema_with_client>

    def test_change_cinema_with_client(self):
        session = self.client.session
        session["selected_cinema"] = self.cinema1.id
        session.save()

        response = self.client.post(reverse("change_cinema"), {"cinema": self.cinema2.id}, HTTP_REFERER="home")

>       self.assertEqual(self.client.session["selected_cinema"], self.cinema2.id)
E       AssertionError: 1 != 2

cinemas_app\tests\test_cinemas_views.py:24: AssertionError

#

this line doesn't seem to work :

response = self.client.post(reverse("change_cinema"), {"cinema": self.cinema2.id}, HTTP_REFERER="home")
jolly rampart
#
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cinephoria.settings")

import django
django.setup()

this shouldn't be necessary in your tests file

#

I would also ask that you share the view code

honest onyx
#

it's a basic view with a form :


from django.shortcuts import redirect
from .forms import CinemaChangeForm


def change_cinema(request):
    selected_cinema = request.session.get('selected_cinema')

    if request.method == 'POST':
        form = CinemaChangeForm(request.POST)
        if form.is_valid():
            selected_cinema = form.cleaned_data['cinema']
            request.session['selected_cinema'] = selected_cinema.id
            return redirect(request.META.get('HTTP_REFERER', '/'))
        return redirect('home')
    return redirect('home')
        
jolly rampart
#

I don't see anything particially incorrect with the view or the test. I would be adding print statements of the session to understand what is going on when running the test

honest onyx
#
self = <test_cinemas_views.ChangeCinemaViewTest testMethod=test_change_cinema_with_client>

    def test_change_cinema_with_client(self):
        session = self.client.session
        print(session)
        session["selected_cinema"] = self.cinema1.id
        session.save()

        response = self.client.post(reverse("change_cinema"), {"cinema": self.cinema2.id}, HTTP_REFERER="home")
        print(response)

>       self.assertEqual(self.client.session["selected_cinema"], self.cinema2.id)
E       AssertionError: 1 != 2

cinemas_app\tests\test_cinemas_views.py:23: AssertionError
-------------------------------------------------------------------------------------- Captured stdout call -------------------------------------------------------------------------------------- 
Cinema A
Cinema B
<django.contrib.sessions.backends.db.SessionStore object at 0x000002004647C440>
<HttpResponseRedirect status_code=302, "text/html; charset=utf-8", url="/">
#

both cinema objects are ok. Session is set. response is a 302 (redirect if I'm not mistaken)

#

So this is clearly the line "response = self.client.post(reverse("change_cinema"), {"cinema": self.cinema2.id}, HTTP_REFERER="home")" not changing the selected cinema but I don't know why

jolly rampart
#

I would add temporary print statements to the view as well. essentially this: print(request.session.get('selected_cinema')) multiple times to track what is happening

honest onyx
#

I will try but that form works well in the app in itself

jolly rampart
#

hmm, my gut tells me the form isn't valid, so printing form.errors after the is_valid() call might reveal something

honest onyx
#

man.... ahahah. I found it

#
django: version: 5.1.5, settings: cinephoria.settings (from ini)
rootdir: D:\Dev\Projects\Python\Django\Cinephoria-Project
configfile: pytest.ini
plugins: django-4.9.0
collected 6 items

cinemas_app/tests/test_cinemas_models.py::CinemaModelTest::test_cinema_creation PASSED                                                                                                      [ 16%]
cinemas_app/tests/test_cinemas_models.py::AuditoriumModelTest::test_auditorium_creation PASSED                                                                                              [ 33%] 
cinemas_app/tests/test_cinemas_views.py::ChangeCinemaViewTest::test_change_cinema_with_client PASSED                                                                                        [ 50%]
movies_app/tests/test_movies_models.py::MovieModelTest::test_movie_creation PASSED                                                                                                          [ 66%] 
movies_app/tests/test_movies_models.py::RatingModelTest::test_rating_creation PASSED                                                                                                        [ 83%]
users_app/tests/test_users_models.py::CustomUserTest::test_user_creation PASSED                                                                                                             [100%]
#

You wanna know what happened ?

I have a nav with a footer loading selected cinema info so you can have address, phone, openings etc in the bottom of the page.
I was in the document inspector and I see a non critical error "you have duplicate form id" or something like this. I click on the two rised sections and I see they're both nav... I'm like "what ??? But I only have one form loading, and why does nav appears two times ?"

Then looking at my template :

header>
    {% include 'partials/nav.html' %}
</header>
<body class="bg-dark">
    {% if messages %}
      <div class="message-container">
        {% for message in messages %}
          <div class="alert alert-{{ message.tags }}">
            {{ message }}
          </div>
        {% endfor %}
      </div>
    {% endif %}
    <div id="navbar">
        {% include 'partials/nav.html' %}
    </div>

Two nav include ! Nice one moron !

#

Someone slap me with a morgenstern please

#

And with great surprise, my test works a lot better with only one nav form !

jolly rampart
#

Happens to the best of us ๐Ÿ˜„

honest onyx
#

Thanks for your help. Finally resolved this and by the way, an old display artifact on the button I did not understand at the time. Now I understand why no amount of CSS and JS could solve it xD

honest onyx
#

I have a last question, sorry. Everything works well locally but I have a message telling my SECRET_KEY is empty when I use linters in my gitlab CI pipeline. BUT, I do pass it as a variable :

# Tests unitaires avec Django et mesure de la couverture
test:
  stage: test
  before_script:
    - python3 -m venv venv
    - source venv/bin/activate
    - pip install -r requirements.txt
    - pip install pytest pytest-django pytest-cov
  script:
    - pytest --maxfail=1 --disable-warnings -q --cov=mon_app  # Mesure la couverture de tests
  variables:
    SECRET_KEY: $SECRET_KEY
    DATABASE_ENGINE: 'sqlite3'  # Utilisation de SQLite3 pour les tests
    DATABASE_NAME: 'cinephoria.db'
  allow_failure: false

FAILED cinemas_app/tests/test_cinemas_views.py::ChangeCinemaViewTest::test_change_cinema_with_client - django.core.exceptions.ImproperlyConfigured: The SECRET_KEY setting must not be empty.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!

jolly rampart
#

In that case I would just set something in the yaml file. In this case it doesn't matter if it's insecure

honest onyx
#

variables:
SECRET_KEY: 'Secret Key Bidon'

Tried it. failed with same error

jolly rampart
#

hmm, can you share the full traceback?

honest onyx
#
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
self = <LazySettings "cinephoria.settings">, name = 'SECRET_KEY'
    def __getattr__(self, name):
        """Return the value of a setting and cache it in self.__dict__."""
        if (_wrapped := self._wrapped) is empty:
            self._setup(name)
            _wrapped = self._wrapped
        val = getattr(_wrapped, name)
    
        # Special case some settings which require further modification.
        # This is done here for performance reasons so the modified value is cached.
        if name in {"MEDIA_URL", "STATIC_URL"} and val is not None:
            val = self._add_script_prefix(val)
        elif name == "SECRET_KEY" and not val:
>           raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")
E           django.core.exceptions.ImproperlyConfigured: The SECRET_KEY setting must not be empty.
venv/lib/python3.11/site-packages/django/conf/__init__.py:90: ImproperlyConfigured
---------- coverage: platform linux, python 3.11.2-final-0 -----------
=========================== short test summary info ============================
FAILED cinemas_app/tests/test_cinemas_views.py::ChangeCinemaViewTest::test_change_cinema_with_client - django.core.exceptions.ImproperlyConfigured: The SECRET_KEY setting must not be empty.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 2 passed in 0.85s
Cleaning up project directory and file based variables
00:00
ERROR: Job failed: exit status 1
#

Reminder of the initial config :

env = environ.Env()
environ.Env.read_env(env_file)

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
jolly rampart
#

Your looking for "DJANGO_SECRET_KEY" and you have named the environment variable "SECRET_KEY"

honest onyx
#

.................................................

#

Do I have mitigating circumstances if I've slept less than 3 hours last night ?