Need help with the Tests - Django tutorial part - 5

Hi, everyone I am Gyan. I have been doing the Django docs tutorials. I coudnt seem to figure out how to resolve these problem in my test.

views:

# importing all the modules below.....
from typing import Any
from django.db.models.query import QuerySet
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, HttpResponseRedirect
from django.template import loader
from django.urls import reverse
from django.http import Http404
from django.utils import timezone

# F() is used to avoid the race condition.
from django.db.models import F

from django.views import generic

# from .models import Question, Choice
from polls.models import Question, Choice


""" 
In our poll application, we'll have the following four views:

Question “index” page - displays the latest few questions.
Question “detail” page - displays a question text, with no results but with a form to vote.
Question “results” page - displays results for a particular question.
"Vote" action - handles voting for a particular choice in a particular question. 
"""

# We have made "function based views" first.

""" 
def index(request):
    # Below "latest_question_list" is intended to get us 5 questions published most recently.
    # And in descending order by there published date. "-" sign indicates descending.
    latest_question_list = Question.objects.order_by("-published_date")[:5]

    # uncomment output when not using any django created template.
    # output = ", ".join([q.question_text for q in latest_question_list])

    '''
    template = loader.get_template("polls/index.html")
    context = {"latest_question_list": latest_question_list}
    return HttpResponse(template.render(context, request))
    '''

    # shortcut for above code.
    context = {"latest_question_list": latest_question_list}
    return render(request, "polls/index.html", context)


def detail(request, question_id):
    '''
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist...")
    '''

    # or there is a shortcut for writing above lines of code by using ....
    question = get_object_or_404(Question, pk=question_id)

    # at last
    return render(request, "polls/detail.html", {"question": question})


def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/results.html", {"question": question})

"""


def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST["choice"])
    except (KeyError, Choice.DoesNotExist):
        return render(
            request,
            "polls/detail.html",
            {"question": question, "error_message": "You didn't select a choice."},
        )
    else:
        selected_choice.vote = F("vote") + 1
        selected_choice.save()
        return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))


# Now,below we are creating "class based views"...
# Below, I am going to refactor the code of 'index, results, detail' view to convert them into
# generic views which are class based view.
class IndexView(generic.ListView):
    template_name = "polls/index.html"
    context_object_name = "latest_question_list"

    def get_queryset(self) -> QuerySet[str]:
        # last 5 question chahiye, thats why we used "-"published_date
        # published_date__lte : here "__lte" means 'less-than-or-equal-to'.
        present_questions = Question.objects.filter(
            published_date__lte=timezone.now()
        ).order_by("-published_date")[:5]

        question_set = Question.objects.none()

        # only questions with choices to be displayed on the screen
        for question in present_questions:
            number_of_choices = len(question.choice_set.all())
            if number_of_choices > 0:
                question_set |= Question.objects.filter(pk=question.pk)

        return question_set


class DetailView(generic.DetailView):
    model = Question
    template_name = "polls/detail.html"

    def get_queryset(self) -> QuerySet[str]:
        """
        Excludes any questions that aren't published yet.
        """
        return Question.objects.filter(published_date__lte=timezone.now())


class ResultsView(generic.DetailView):
    model = Question
    template_name = "polls/results.html"

    def get_queryset(self) -> QuerySet[str]:
        return Question.objects.filter(published_date__lte=timezone.now())

tests:

import datetime
from django.utils import timezone
from django.test import TestCase
from django.urls import reverse
from django.db.models.fields import AutoField

from .models import Question, Choice


# creating a function to create Question for us....
def createQuestion(question_text, days, id=None):
    # negative for questions published in the past, positive for questions that have yet to be published
    published_date = timezone.now() + datetime.timedelta(days=days)
    question = Question(
        question_text=question_text, published_date=published_date, id=id
    )
    return question


# self.client ---> we get a client from TestCase class that we inherited to make test classes.
# self ---> contains reference of the object created by "QuestionModelTests" and "TestCase" class
#           because when we inherit a class that means we are using functions and variables of
#           that class.
class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        # question going to be published in 30 days.
        future_question = createQuestion(question_text="Future Question.", days=30)
        self.assertIs(future_question.was_published_recently(), False)  # --> better
        # or,
        # assert future_question.was_published_recently() == False

    def test_was_published_recently_with_recent_date(self):
        # question going to be published in right now.
        recent_question = createQuestion(question_text="recent question.", days=0)
        self.assertIs(recent_question.was_published_recently(), True)

    def test_was_published_recently_with_old_question(self):
        # question published in 2 days ago.
        old_question = createQuestion(question_text="old question.", days=-2)
        self.assertIs(old_question.was_published_recently(), False)


class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        response = self.client.get(reverse("polls:index"))
        self.assertEqual(response.status_code, 200)
        self.assertContains(
            response, "No polls are available.But you are welcome to make one!"
        )
        self.assertQuerySetEqual(response.context["latest_question_list"], [])

        # 'response.context["latest_question_list"]' -----> going to return the list of questions
        # we get from "get_querySet()" method of Index view.

    def test_past_question(self):
        question = createQuestion(question_text="Past Question.", days=-10)
        choice1 = Choice(question=question, choice_text="Yes/No")
        choice2 = Choice(question=question, choice_text="haan/naa")
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(response.context["latest_question_list"], [question])

    def test_future_question(self):
        future_question = createQuestion(question_text="Future Question.", days=20)
        choice = Choice(question=future_question, choice_text="Yes/No")
        response = self.client.get(reverse("polls:index"))
        self.assertContains(
            response,
            "No polls are available.But you are welcome to make one!",
        )
        self.assertQuerySetEqual(response.context["latest_question_list"], [])

    def test_past_question_and_future_question(self):
        past_question = createQuestion(question_text="Past Question", days=-30)
        future_question = createQuestion(question_text="Future Question", days=30)
        choice1 = Choice(question=past_question, choice_text="Yes/No")
        choice2 = Choice(question=future_question, choice_text="haan/Naa")
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"], [past_question]
        )

    # we need to write test for "multiple past question", which also means test for more than 1 question.
    def test_two_past_questions(self):
        question1 = createQuestion(question_text="Past Question 1", days=-2)
        question2 = createQuestion(question_text="Past Question 2", days=-3)
        choice1 = Choice(question=question1, choice_text="Yes/No")
        choice2 = Choice(question=question2, choice_text="Haan/Naa")
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"], [question1, question2]
        )


# Note that the "django.test.TestCase" class provides some additional "assertion" methods.
# This means assertQuerySetEqual, assertContains, assertIs, assertEqual we are using these methods because we inherited the class "TestCase"  in all of our Test classes.


# Now we are going to write Tests for "DetailView"
# 1) Test to make sure past question is displayed flawlessly.
# 2) Test to make sure future question is not displayed.
class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        The detail view of a question with a pub_date in the future
        returns a 404 not found.
        """
        future_question = createQuestion(question_text="Future question.", days=5, id=1)
        choice = Choice(question=future_question, choice_text="y/N")
        url = reverse("polls:detail", args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        The detail view of a question with a pub_date in the past
        displays the question's text.
        """
        past_question = createQuestion(question_text="Past Question.", days=-5, id=2)
        choice = Choice(question=past_question, choice_text="y/N")
        url = reverse("polls:detail", args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)


class QuestionResultViewTests(TestCase):
    # 1) test the past question publishing in Result View.
    # 2) test the future question publishing in Result View.

    def test_past_question(self):
        past_question = createQuestion(question_text="Past Question.", days=-30, id=1)
        choice = Choice(question=past_question, choice_text="y/N")
        url = reverse("polls:results", args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)

    def test_future_question(self):
        future_question = createQuestion(question_text="Future Question.", days=1, id=1)
        choice = Choice(question=future_question, choice_text="y/N")
        url = reverse("polls:results", args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

Error:

Found 12 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.F..FFF....F
======================================================================
FAIL: test_past_question (polls.tests.QuestionDetailViewTests.test_past_question)
The detail view of a question with a pub_date in the past
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\GYAN\Desktop\Django\Django Docs Tutorials\mysite\polls\tests.py", line 115, in test_past_question
    self.assertContains(response, past_question.question_text)
  File "C:\Users\GYAN\Desktop\Django\Django Docs Tutorials\django-tutorials\Lib\site-packages\django\test\testcases.py", line 647, in assertContains
    text_repr, real_count, msg_prefix = self._assert_contains(
                                        ^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\GYAN\Desktop\Django\Django Docs Tutorials\django-tutorials\Lib\site-packages\django\test\testcases.py", line 610, in _assert_contains
    self.assertEqual(
AssertionError: 404 != 200 : Couldn't retrieve content: Response code was 404 (expected 200)

======================================================================
FAIL: test_past_question (polls.tests.QuestionIndexViewTests.test_past_question)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\GYAN\Desktop\Django\Django Docs Tutorials\mysite\polls\tests.py", line 60, in test_past_question
    self.assertQuerySetEqual(response.context["latest_question_list"], [question])
  File "C:\Users\GYAN\Desktop\Django\Django Docs Tutorials\django-tutorials\Lib\site-packages\django\test\testcases.py", line 1346, in assertQuerySetEqual
    return self.assertEqual(list(items), values, msg=msg)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Lists differ: [] != [<Question: Past Question.>]

Second list contains 1 additional elements.
First extra element 0:
<Question: Past Question.>

- []
+ [<Question: Past Question.>]

======================================================================
FAIL: test_past_question_and_future_question (polls.tests.QuestionIndexViewTests.test_past_question_and_future_question)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\GYAN\Desktop\Django\Django Docs Tutorials\mysite\polls\tests.py", line 75, in test_past_question_and_future_question
    self.assertQuerySetEqual(response.context["latest_question_list"], [question])
  File "C:\Users\GYAN\Desktop\Django\Django Docs Tutorials\django-tutorials\Lib\site-packages\django\test\testcases.py", line 1346, in assertQuerySetEqual
    return self.assertEqual(list(items), values, msg=msg)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Lists differ: [] != [<Question: Past Question>]

Second list contains 1 additional elements.
First extra element 0:
<Question: Past Question>

- []
+ [<Question: Past Question>]

======================================================================
FAIL: test_two_past_questions (polls.tests.QuestionIndexViewTests.test_two_past_questions)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\GYAN\Desktop\Django\Django Docs Tutorials\mysite\polls\tests.py", line 82, in test_two_past_questions
    self.assertQuerySetEqual(
  File "C:\Users\GYAN\Desktop\Django\Django Docs Tutorials\django-tutorials\Lib\site-packages\django\test\testcases.py", line 1346, in assertQuerySetEqual
    return self.assertEqual(list(items), values, msg=msg)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Lists differ: [] != [<Question: Past Question 1>, <Question: Past Question 2>]

Second list contains 2 additional elements.
First extra element 0:
<Question: Past Question 1>

- []
+ [<Question: Past Question 1>, <Question: Past Question 2>]

======================================================================
FAIL: test_past_question (polls.tests.QuestionResultViewTests.test_past_question)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\GYAN\Desktop\Django\Django Docs Tutorials\mysite\polls\tests.py", line 127, in test_past_question
    self.assertContains(response, past_question.question_text)
  File "C:\Users\GYAN\Desktop\Django\Django Docs Tutorials\django-tutorials\Lib\site-packages\django\test\testcases.py", line 647, in assertContains
    text_repr, real_count, msg_prefix = self._assert_contains(
                                        ^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\GYAN\Desktop\Django\Django Docs Tutorials\django-tutorials\Lib\site-packages\django\test\testcases.py", line 610, in _assert_contains
    self.assertEqual(
AssertionError: 404 != 200 : Couldn't retrieve content: Response code was 404 (expected 200)

----------------------------------------------------------------------
Ran 12 tests in 0.052s

FAILED (failures=5)
Destroying test database for alias 'default'...

Can anyone please help me. :sob:

Your createQuestion method does not match what the tutorial instructs you to use.

Understanding the difference between the code you’re written and what’s in the tutorial is critical.

(Side note: I also strongly encourage you to adopt the Python / Django naming conventions for classes and functions. This function should be named create_question and not createQuestion. This difference in names is not the issue I’m referencing above, and technically not a “problem”, it’s just a violation of long-standing naming conventions.)

Thank you @KenWhitesell for your response.
I understand the function that is given in the tutorial. It is as follows.

def create_question(question_text, days):
"""
Create a question with the given `question_text` and published the
given number of `days` offset to now (negative for questions published
in the past, positive for questions that have yet to be published).
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)

However, I have encountered an issue with this function. It does not assign an id to the question that is created. This causes a problem with the reverse() function, which requires the question’s id as an argument. I get a NoReverseMatch exception when I try to use it.
How can i resolve it?

I appreciate your feedback on the importance of naming conventions in django. I am still learning the best practices of this framework, so I will pay more attention to them in the future. Thank you for your patience and guidance.

First, I’ll start by asking the more fundamental question. Do you understand the difference between what you had written for createQuestion, and what the tutorial instructs you to do in create_question - and why your code doesn’t work?

(It’s ok if you don’t - we can cover this topic. But it is a crucial point.)

We would need to see the code that you tried to be able to figure out what is wrong. If you were using your createQuestion method, it would have been because of the flaw in that method, not because you weren’t passing an id field.

You could add the id field to the create call - but it’s not necessary, and would be considered an anti-pattern.

Notice that the tutorial does not assign an id but still works perfectly.

Read the tests code to see where and how it gets the id for the url reverse function.

Do you understand the difference between what you had written for `createQuestion` , and what the tutorial instructs you to do in `create_question` - and why your code doesn’t work?
So, the difference I understood between these 2 methods is that createQuestion explicitly passed the id and other create_question , in this method id is provided by django automatically.
If I am missing something in my interpretation so, please correct me. :blush:

I tried tutorial’s create_question in place of my createQuestion . And all the tests passed. Thank you for pin pointing my mistakes.

One more thing I need help with, from tutorial - 5.
Tutorial - 5 gave an Idea for improving voting app. It suggested that …
We could also improve our application in other ways, adding tests along the way. For example, it’s silly that `Questions` can be published on the site that have no `Choices` . So, our views could check for this, and exclude such `Questions` . Our tests would create a `Question` without `Choices` and then test that it’s not published, as well as create a similar `Question` *with* `Choices` , and test that it *is* published.

So, the logic I wrote in the IndexView is below to implement the above idea…

class IndexView(generic.ListView):
    template_name = "polls/index.html"
    context_object_name = "latest_question_list"

    def get_queryset(self) -> QuerySet[str]:
        # last 5 question chahiye, thats why we used "-"published_date
        # published_date__lte : here "__lte" means 'less-than-or-equal-to'.
        present_questions = Question.objects.filter(
            published_date__lte=timezone.now()
        ).order_by("-published_date")[:5]

        question_set = Question.objects.none()

        # only questions with choices to be displayed on the screen
        for question in present_questions:
            number_of_choices = len(question.choice_set.all())
            if number_of_choices > 0:
                question_set |= Question.objects.filter(pk=question.id)

        return question_set

Now, this logic is correct I have manually tested it by created a question without choices and looking up on the indexView template if it is present or not.
But I couldn’t be able to create instances of Choice model in Tests to test my logic out.

Tests

import datetime
from django.utils import timezone
from django.test import TestCase
from django.urls import reverse

from .models import Question, Choice


# creating a function to create Question for us....
def create_question(question_text, days):
    """
    Create a question with the given `question_text` and published the
    given number of `days` offset to now (negative for questions published
    in the past, positive for questions that have yet to be published).
    """
    published_date = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(
        question_text=question_text, published_date=published_date
    )


# self.client ---> we get a client from TestCase class that we inherited to make test classes.
# self ---> contains reference of the object created by "QuestionModelTests" and "TestCase" class
#           because when we inherit a class that means we are using functions and variables of
#           that class.
class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        # question going to be published in 30 days.
        future_question = create_question(question_text="Future Question.", days=30)
        self.assertIs(future_question.was_published_recently(), False)  # --> better
        # or,
        # assert future_question.was_published_recently() == False

    def test_was_published_recently_with_recent_date(self):
        # question going to be published in right now.
        recent_question = create_question(question_text="recent question.", days=0)
        self.assertIs(recent_question.was_published_recently(), True)

    def test_was_published_recently_with_old_question(self):
        # question published in 2 days ago.
        old_question = create_question(question_text="old question.", days=-2)
        self.assertIs(old_question.was_published_recently(), False)


class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        response = self.client.get(reverse("polls:index"))
        self.assertEqual(response.status_code, 200)
        self.assertContains(
            response, "No polls are available.But you are welcome to make one!"
        )
        self.assertQuerySetEqual(response.context["latest_question_list"], [])

        # 'response.context["latest_question_list"]' -----> going to return the list of questions
        # we get from "get_querySet()" method of Index view.

    def test_past_question(self):
        question = create_question(question_text="Past Question.", days=-10)
        Choice(question=question, choice_text="Yes/No")
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(response.context["latest_question_list"], [question])

    def test_future_question(self):
        create_question(question_text="Future Question.", days=20)
        response = self.client.get(reverse("polls:index"))
        self.assertContains(
            response,
            "No polls are available.But you are welcome to make one!",
        )
        self.assertQuerySetEqual(response.context["latest_question_list"], [])

    def test_past_question_and_future_question(self):
        past_question = create_question(question_text="Past Question", days=-30)
        Choice(question=past_question, choice_text="Yes/No")
        future_question = create_question(question_text="Future Question", days=30)
        Choice(question=future_question, choice_text="Haan/Naa")
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"], [past_question]
        )

    # we need to write test for "multiple past question", which also means test for more than 1 question.
    def test_two_past_questions(self):
        question1 = create_question(question_text="Past Question 1", days=-2)
        Choice(question=question1, choice_text="Yes/No")
        self.assertEqual(len(question1.choice_set.all()), 1)
        question2 = create_question(question_text="Past Question 2", days=-3)
        Choice(question=question2, choice_text="Haan/Naa")
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"], [question1, question2]
        )


# Note that the "django.test.TestCase" class provides some additional "assertion" methods.
# This means "assertQuerySetEqual", "assertContains", "assertIs", "assertEqual" we are using these methods because we inherited the class "TestCase"  in all of our Test classes.


# Now we are going to write Tests for "DetailView"
# 1) Test to make sure past question is displayed flawlessly.
# 2) Test to make sure future question is not displayed.
class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        The detail view of a question with a pub_date in the future
        returns a 404 not found.
        """
        future_question = create_question(question_text="Future question.", days=5)
        url = reverse("polls:detail", args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        The detail view of a question with a pub_date in the past
        displays the question's text.
        """
        past_question = create_question(question_text="Past Question.", days=-5)
        url = reverse("polls:detail", args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)


class QuestionResultViewTests(TestCase):
    # 1) test the past question publishing in Result View.
    # 2) test the future question publishing in Result View.

    def test_past_question(self):
        past_question = create_question(question_text="Past Question.", days=-30)
        url = reverse("polls:results", args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)

    def test_future_question(self):
        future_question = create_question(question_text="Future Question.", days=1)
        url = reverse("polls:results", args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

And sorry for replying late. I am a little busy with my exams.
Thank you @KenWhitesell.

Yep - the fundamental difference is that the tutorial has this line:
return Question.objects.create(question_text=question_text, pub_date=time)

This creates an instance of Question and saves it in the database. (See the docs at create)

However, what you had was:

Which creates the instance of Question, but does not write it to the database.

And that’s why the tests relying on it would fail. You would try to issue a query for that Question object, but it wouldn’t be found, because it never existed in the database.

Side note: You want to avoid doing things like this. Use the count method to identify the number of rows that would be returned by a query.

I’m not sure I’m following what you’re asking here. You would create one (or more) Choice instances in the same way that you create the Question instance. If you’ve tried something that doesn’t work, please post what you tried along with the error message received.

You never need to apologize for a “late” or “delayed” response. We’re here to help and we’ll make whatever progress our mutual schedules permit.

1 Like

Oh, this is the mistake I am doing , I am just creating the instance of Question and not really saving it to database. But with create() method , we can both create instance and save it to database at the same time. Thank you, appreciate it.:heart:

Cool, I will avoid doing things like this✌️.

So, I did exactly the above mistake here also. I instantiated Choice model without using the create() method. Thanks again.

Absolutely, thanks for all the help @KenWhitesell.:heart: