Learning history for participating in team app development in Python ~ Django Tutorial 5 ~

Introduction

This time, I will touch on testing apart from the function of the app.

About the test

I used to make this kind of thing for job hunting, but when I actually made it public and had the person in charge see it at the interview, or from the person who touched it, the framework Many people pointed out that they did not understand enough. One of them was that there was no test code.

From the official tutorial

Why you need to create a test Why make a test? And why now? Perhaps you're too busy learning Python or Django, and learning something else can seem daunting and unnecessary. That's because the voting application works fine, and the introduction of automated testing doesn't make the application better. If your only purpose in learning Django programming is to create this voting application, you certainly don't need to implement automated testing. But if not, now is the perfect time to learn about automated testing.

The official tutorial also introduces the test in this way. So why write a test

--If you automate and incorporate algorithm verification, if you make changes to any of the elements that make up the system, it will not work properly, or if not, what will be the problem? You can immediately grasp whether or not.

--Conversely, being able to write code that verifies an algorithm proves that you have a good understanding of the algorithm.

I felt that it was roughly divided into these two points. The former is important from the perspective of so-called integrity and maintenance. Especially if you are a beginner like us

From the official tutorial

Writing tests is useful for team collaboration. So far, I've written from the perspective of one developer maintaining the application. However, complex applications are becoming team-maintained. Testing protects you from accidentally breaking code you write (and also from you breaking code written by others). If you're going to live as a Django programmer, you definitely have to write good tests!

I tend to lack this kind of thinking, but even if I do it by myself, I will do it as a team because yesterday's self, my typo a few hours ago, and strange code will hinder the code I wrote now. When it comes to that, it's not hard to imagine that there is a risk.

Regarding the latter, I felt that it was only after the test that I could call myself a coder. Let's move on.

When should a test be created?

  1. Opinion to write the test and then the code → It is based on the theory that if you write a test, you can write code. Clarify the problem with the process you want to achieve before you start writing code.

  2. Opinion to write code and then test → Write according to the theory of checking whether the code you wrote works normally.

  3. Add new features, check bugs, and modify existing components.

I don't know which is the correct answer, but I think anyone who can do 1 is already a good programmer. Then, I think that the goal of us beginners is to be conscious of going to the third stage, which is a compromise between 1 and 2.

Taking the time to write a test is a good detour from the point of view of simply "making something that moves", and it is a place that beginners feel awkward.

However, as I said repeatedly, if you can't write a test, you can't help being evaluated by others that you don't understand the code you wrote, so the first thing you should do to get out of it. I think that corresponds to 3.

Let's write a bug check test code.

Now, let's take a look at the Django tutorial test items, keeping in mind what we wrote in 3. Remember that there was a bug in models.py in previously? This is a bug that the so-called ** future date ** is displayed.

Execute the following command using the shell command.

import datetime
from django.utils import timezone
from polls.models import Question

future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30)) #1
future_question.was_published_recently() #2

In 1, the timedelta method is used to create an instance of a future date. Since pub_date = timezone.now () + datetime.timedelta (days = 30), we will publish an instance with date information 30 days after the current date.

In 2, the was_published_recently () method is executed using that instance. What are you doing

return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

It was a method that returns True if the date information of the instance is not a past date, otherwise it returns False. However, it was said that there is a bug that it will be True even if it is a future date. So the result of 2 is also True. Now let's create a test to find this bug.

polls/tests.py


import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question


class QuestionModelTests(TestCase):

    def test_was_published_recently_with_future_question(self):

        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

The file that describes tests in Django is tests.py. Let's import the TestCase object at the beginning. The way to write a test is to create a class that inherits django.test.TestCase and write the method there. By the way, at the same time as inheriting TestCase, it also inherits TransactionTestCase and SimpleTestCase. For apps that do not use a database, it seems better to inherit only SimpleTestCase. So, to put it simply, what this TestCase does is that when tests.py is executed, it starts searching for classes that inherit from TestCase. And when you discover it

  1. Create a database for testing and apply the migration.
  2. Find and execute the method that starts with test.
  3. Roll back.
  4. Repeat steps 2 and 3 if there are other methods.
  5. Roll back to before executing the TestCase class.
  6. Delete the database created in 1.

Is performed. In other words, it can be said that the test and each method in the test are somewhat independent from the viewpoint of the application. Reference 1 Reference 2

Next, what is the test_was_published_recently_with_future_question method doing?

  1. Assign a future date to the time variable.
  2. Assign the Question instance with the date information of the time variable to the future_question variable.
  3. ʻassertIs (a, b) verifies that ʻa = b, that is, future_question.was_published_recently () is False.

It means that. Now that you understand this, let's run the test.

py manage.py test polls

If you have multiple applications, replace polls with any application folder name. As an execution result

System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
    self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False

----------------------------------------------------------------------
Ran 1 test in 0.001s

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

You should see something like this. You can see here that was_published_recently () has returned True for a future date, as we are getting the error ʻAssertionError: True is not False`. You found a bug.

Bug fixes

Now that we've found a bug, we'll write code to fix it.

# return self.pub_date >= timezone.now() - datetime.timedelta(days=1)To the following

def was_published_recently(self):
    now = timezone.now()
    return now - datetime.timedelta(days=1) <= self.pub_date <= now

If you want to return True to a future date, you can add the current date to the condition specification and set it to the maximum value.

By the way, when it comes to rigorous testing

polls/tests.py


def test_was_published_recently_with_old_question(self):
    """
    was_published_recently() returns False for questions whose pub_date
    is older than 1 day.
    """
    time = timezone.now() - datetime.timedelta(days=1, seconds=1)
    old_question = Question(pub_date=time)
    self.assertIs(old_question.was_published_recently(), False)

def test_was_published_recently_with_recent_question(self):
    """
    was_published_recently() returns True for questions whose pub_date
    is within the last day.
    """
    time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
    recent_question = Question(pub_date=time)
    self.assertIs(recent_question.was_published_recently(), True)

Define the above two methods in tests.py. The test_was_published_recently_with_old_question () method is a method that verifies that was_published_recently () has not returned True in the past date. The test_was_published_recently_with_recent_question () method is a method that verifies that was_published_recently () is returning True on the current date.

Write a test code (View)

Now that we've tested the algorithm, you'll be wondering if it actually works in your application. So, this time I will write a test for View.

First, add the following modules and test classes to the class you just created.

pols/tests.py


from django.urls import reverse

#Omission

def create_question(question_text, days):
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)


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.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    def test_past_question(self):
    	#Create a Question instance with a past date
        create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question.>']
        )

    def test_future_question(self):
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    def test_future_question_and_past_question(self):
        create_question(question_text="Past question.", days=-30)
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question.>']
        )

    def test_two_past_questions(self):
        create_question(question_text="Past question 1.", days=-30)
        create_question(question_text="Past question 2.", days=-5)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question 2.>', '<Question: Past question 1.>']
        )

The create_question method is a method for creating a Question object that takes question_text and days as arguments. Since the Question object will be created in the subsequent test classes, define it at the beginning so that you do not have to write the code each time.

Let's take a look at the QuestionIndexViewTests class. The client class that appears throughout the whole story is a class used for testing and is a class that simulates access in the browser. Reference For example, writing self.client.get will test the actual request processing like self.objects.get.

test_no_questions is a method that verifies the behavior when the Question object does not exist. Now hit the URL with response = self.client.get (reverse ('polls: index')).

    self.assertEqual(response.status_code, 200)
    self.assertContains(response, "No polls are available.")
    self.assertQuerysetEqual(response.context['latest_question_list'], [])

The above three verify the arguments in the same way as the ʻassertIs ()method that came out earlier. For example,self.assertEqual (response.status_code, 200)will verify whether the page is displayed in the browser as a result of hitting the URL earlier. Since the access code of 200 is issued when the page is displayed normally, we are verifyingresponse.status_code == 200. By the way, when checking the operation of POST such as form, it will be self.client.post`.

self.assertContains () verifies whether the argument contains the one specified by the second argument. For example, in this case, context is included in the return value ofself.client.get (reverse ('polls: index'))in response, so the character string specified in it. It means to verify whether there is.

self.assertQuerysetEqual () checks if there is data in the queryset. By the way, a query set refers to a data type like ʻint, and more roughly, think of it as a series of information taken from a model. This time, we hit the URL in the path of polls: index, so the ʻIndexView class will be executed. Then, in the ʻIndexView class, the data is finally fetched from the database by the get_queryset () method, so the query set is born there. By the way Since self.assertQuerysetEqual (response.context ['latest_question_list'], []), in this case it means that latest_question_list` is empty, that is, it verifies that there are no questions.

The same applies to subsequent test methods. Create a Question object by specifying the values of question_text and days as arguments to the definedcreate_question ()method, and test based on it. Regarding the execution result of response.context (), in the template

<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

Since I made such a description, I specified question_text as an argument, so I remember that the one specified there is entered in{{question.question_text}}. So, for example, the test_past_question () method will put the Past question. here. Therefore

self.assertQuerysetEqual(response.context['latest_question_list'],['<Question: Past question.>'])

It's important to understand that such a statement means verifying that the return value contains Past question. in the html context inside response. If you understand so far, in test_future_question ()

self.assertQuerysetEqual(response.context['latest_question_list'], [])

I think it's not difficult because you can see that is true.

Finally, modify View before testing. I've fixed the model bug, but it's because it would end up with an object with a future date. Import the timezone object from the django.utils module and modify the ʻIndexView` class as follows:


from django.utils import timezone

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

	def get_queryset(self):
		return Question.objects.filter(
			pub_date__lte = timezone.now()
			).order_by('-pub_date')[:5]

Think of pub_date__lte = timezone.now () as synonymous with pub_date <= timezone.now (). In other words, the process returns a query set that retrieves 5 Question objects with date information before the current date.

DetailView test

Although it is okay to correct up to ʻIndex, the data of the future date will still be displayed in DetailView` as it is, so let's also correct it here. Modify as follows.

polls/views.py



class DetailView(generic.DetailView):
    ...
    def get_queryset(self):
        return Question.objects.filter(pub_date__lte=timezone.now())

Next, add the following test class to tests.py.

class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        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):
        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

Basically, the theory is the same as the previous test. The difference is the DetailView test, so prepare the 34 part of the URL such as / polls / 34 in advance in the form of ʻurl = reverse ('polls: detail', args = (future_question.id,)) `. You have to specify it as a separate URL.

What if there are too many tests?

By the way, some of you may have noticed by writing some tests so far, but for example, in the case of the application made in this tutorial, you have to write a test for resluts.html, that is, the ResultsView class. I know you can't. However, you can see that the content of the test is almost the same as the test for the DetaiView class I just wrote. The only difference is that the polls: detail part is changed to polls: results.

On the other hand, the official document considers that duplication is inherent in the test, and presents the following three points as rules for organizing.

--Split TestClass by model or view --Create different test methods for each set of conditions you want to test --The name of the test method should be something that describes its function.

Supplement

I'll leave you with the knowledge you needed to run this test tutorial.

Copying files between container and host in Docker

・ From container to host

docker cp container name or ID: path to the directory or file you want to copy the directory of the destination host

・ From host to container

docker cp path to the directory or file you want to copy Container name or ID: path to save to the container

docker exec -it container name/bin/bash

Enter the container with, etc.

py -c "import django; print(django.__path__)"

So you have to search the path of the source file. You can find the location of the Django folder by searching the path of the source file, but if you copy it to the host side, you can find it on the host side at worst. Of course, if you know the directory up to the file, you can just add it and copy only that file directly.

Finally

I studied the test code for the first time this time, but I felt that it was quite profound because I could finally call myself a coder after writing this. The class-based view was also confusing, but I don't think this is enough. However, when I wrote the test code in this way, I reconfirmed the code I had written so far, and I felt that it was important because I deepened my understanding again, including the parts that were unclear. As a supplement to the tutorial, there is a chapter that delves into only the test, so I would like to read it after the following chapters 6 and 7 are over.

reference

[Docker] Copy files between container and host [[Django] Summary of automated testing](https://qiita.com/okoppe8/items/eb7c3be5b9f6be244549#%E5%87%A6%E7%90%86%E3%81%AE%E6%B5%81% E3% 82% 8C) Creating your first Django app, part 5

Recommended Posts

Learning history for participating in team app development in Python ~ Django Tutorial 5 ~
Learning history for participating in team app development in Python ~ Django Tutorial 4 ~
Learning history for participating in team app development in Python ~ Django Tutorial 1, 2, 3 ~
Learning history for participating in team app development in Python ~ Django Tutorial 6 ~
Learning history for participating in team app development in Python ~ Django Tutorial 7 ~
Learning history for participating in team application development in Python ~ Index page ~
Learning history for participating in team application development in Python ~ Think a little about requirement definition ~
Learning history for participating in team application development in Python ~ Supplement of basic items and construction of jupyterLab environment ~
Learning history to participate in team application development with Python ~ Build Docker / Django / Nginx / MariaDB environment ~
Learning history to participate in team application development in Python ~ After finishing "Introduction to Python 3" of paiza learning ~
Python Django Tutorial (5)
Python Django Tutorial (2)
Python Django Tutorial (8)
Python Django Tutorial (6)
Python Django Tutorial (7)
Python Django Tutorial (1)
Python Django tutorial tutorial
Python Django Tutorial (3)
Python Django Tutorial (4)
Boost.NumPy Tutorial for Extending Python in C ++ (Practice)
[Implementation for learning] Implement Stratified Sampling in Python (1)
Learning notes for the migrations feature in the Django framework (2)
Python Django tutorial summary
Build an interactive environment for machine learning in Python
Directory structure for test-driven development using pytest in python
App development to tweet in Python from Visual Studio 2017
Learning notes for the migrations feature in the Django framework (3)
Learning notes for the migrations feature in the Django framework (1)
Deep Learning Experienced in Python Chapter 2 (Materials for Journals)
Framework development in Python
Development environment in Python
AWS SDK for Python (Boto3) development in Visual Studio 2017
Slackbot development in Python
Tutorial for doing Test Driven Development (TDD) in Flask-2 Decorators
Building a development environment for Android apps-creating Android apps in Python
Tutorial for doing Test Driven Development (TDD) in Flask ―― 1 Test Client
Learning flow for Python beginners
Python learning plan for AI learning
Search for strings in Python
Techniques for sorting in Python
Python development in Visual Studio 2017
Qt for Python app self-update
Python Django Tutorial Cheat Sheet
Checkio's recommendation for learning Python
[For organizing] Python development environment
Python development in Visual Studio
About "for _ in range ():" in python
How about Anaconda for building a machine learning environment in Python?
Automatically resize screenshots for the App Store for each screen in Python
EEG analysis in Python: Python MNE tutorial
Check for memory leaks in Python
Check for external commands in python
8 Frequently Used Commands in Python Django
Web teaching materials for learning Python
Web application development memo in python
[For beginners] Django -Development environment construction-
Widrow-Hoff learning rules implemented in Python
Python development environment options for May 2020
<For beginners> python library <For machine learning>
Emacs settings for Python development environment
Python: Preprocessing in Machine Learning: Overview