【初心者】【Python/Django】駆け出しWebエンジニアがDjangoチュートリアルをやってみた～その5～

# はじめに

みなさん、初めまして。
Djangoを用いた投票 (poll) アプリケーションの作成過程を備忘録として公開していこうと思います。
Qiita初心者ですので読みにくい部分もあると思いますがご了承ください。

シリーズ

- [【初心者】【Python/Django】駆け出しWebエンジニアがDjangoチュートリアルをやってみた～その0～](https://qiita.com/oboerarenai_user/items/4ff87b2afbad8c16ad40)
- [【初心者】【Python/Django】駆け出しWebエンジニアがDjangoチュートリアルをやってみた～その1～](https://qiita.com/oboerarenai_user/items/4357841493832cd6ca2a)
- [【初心者】【Python/Django】駆け出しWebエンジニアがDjangoチュートリアルをやってみた～その2～](https://qiita.com/oboerarenai_user/items/4a36c2b11e7dee74b66b)
- [【初心者】【Python/Django】駆け出しWebエンジニアがDjangoチュートリアルをやってみた～その3～](https://qiita.com/oboerarenai_user/items/db5ef43bcff25223a147)
- [【初心者】【Python/Django】駆け出しWebエンジニアがDjangoチュートリアルをやってみた～その4～](https://qiita.com/oboerarenai_user/items/b942c865e38a46d112c6)

# 作業開始

## 自動テストの導入

Djangoの自動テスト機能で使用するコードを書いていきます。

## 初めてのテスト作成

Djangoのテストとはどのようなコードが必要でしょうか。
まずは対話形式でテストを実行します。

テスト内容：Questionのpub_dateが未来の日付(今日から30日後)になっている場合にwas_published_recently関数がFalseとなることを確認する
テスト結果：FAILD（was_published_recentlyの期待する動作はFalseであるが、実動作はTrueとなっている）

```python

(poll-HcNSSqhc) C:\django\poll>python manage.py shell
Python 3.8.6 (tags/v3.8.6:db45529, Sep 23 2020, 15:37:30) [MSC v.1927 32 bit (Intel)] on win32     
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>>
>>> future_question = Question(pub_date=timezone.now()+datetime.timedelta(days=30))
>>> future_question.was_published_recently()
True
>>>
```

これをコード化します。
self.assertIsで期待する動作はFalseであるとしています。

```python:polls/tests.py

from django.test import TestCase

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

# Create your tests here.


class QuestionModelTests(TestCase):
    def test_was_published_recently_whit_future_question(self):
        future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
        self.assertIs(future_question.was_published_recently(), False)
```

続いてテストを実行しましょう。もちろん対話型ではありませんよ。

```python

(poll-HcNSSqhc) C:\django\poll>python manage.py test polls
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_whit_future_question (polls.tests.QuestionModelTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\django\poll\polls\tests.py", line 13, in test_was_published_recently_whit_future_question
    self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False

----------------------------------------------------------------------
Ran 1 test in 0.002s

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

(poll-HcNSSqhc) C:\django\poll>
```

しっかりとFAILDを返しています。（was_published_recentlyの期待する動作はFalseであるが、実動作はTrueとなっている）

ではデバッグしましょう。

```python:polls/models.py

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

```python

(poll-HcNSSqhc) C:\django\poll>python manage.py test polls
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.002s

OK
Destroying test database for alias 'default'...

(poll-HcNSSqhc) C:\django\poll>
```

OKとなりました。（was_published_recentlyの期待する動作はFalseであり、実動作はFalseとなっている）

## より包括的なテスト

同じクラスに対してさらにテスト項目を追加しましょう。限界値テスト項目を追加しています。

```python:polls/tests.py

    def test_was_published_recently_with_old_question(self):
        old_question = Question(pub_date=timezone.now() - datetime.timedelta(days=1, seconds=1))
        self.assertIs(old_question.was_published_recently(), False)

    def test_was_published_recently_with_recently_question(self):
        recently_question = Question(pub_date=timezone.now(
        ) - datetime.timedelta(hours=23, minutes=59, seconds=59))
        self.assertIs(recently_question.was_published_recently(), True)
```

```python

(poll-HcNSSqhc) C:\django\poll>python manage.py test polls
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...
----------------------------------------------------------------------
Ran 3 tests in 0.004s

OK
Destroying test database for alias 'default'...

(poll-HcNSSqhc) C:\django\poll>
```
OKと表示されました。

## ビューをテストする

DjangoでビューのテストをするためにはClientクラスを使います。
まずは対話形式でテストを実行します。

テスト内容：「http://127.0.0.1/polls」のビューが存在すること
テスト結果：HTTP200、およびコンテンツが取得できている

```python

(poll-HcNSSqhc) C:\django\poll>python manage.py shell
Python 3.8.6 (tags/v3.8.6:db45529, Sep 23 2020, 15:37:30) [MSC v.1927 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 
>>> 
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()  
>>> from django.test import Client
>>> from django.urls import reverse
>>> client = Client()
>>> response = client.get(reverse('polls:index'))      
>>> 
>>> response.status_code
200
>>>
>>> response.content
b'\n    <ul>\n    \n        <li><a href="/polls/5/">What&#x27;s this?</a></li>\n    \n    </ul>\n'
>>>
>>> response.context['latest_question_list'] 
<QuerySet [<Question: What's this?>]>
>>>
```

### ビューを改良する

未来の日付になっている投票を表示する問題がありますので、改修しましょう。

Question.objects.filter()はフィルタすることを意味し、pub_date__lte=timezone.now()はpub_dateがtimezone.now()以下(less than equal)の条件式となります。

```python:polls/views.py

from django.utils import timezone

class IndexView(generic.ListView):
    ***
    def get_queryset(self):
        return Question.objects.filter(pub_date__lte=timezone.now()).order_by('-pub_date')[:5]
```

### 新しいビューをテストする

テストコードを用意します。

```python:polls/tests.py

from django.urls import reverse

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 QuestionIndexViewTest(TestCase):
    # HTTP200、画面メッセージ：No polls are available.、質問リスト：空
    def test_no_question(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'], [])

    # 質問リスト：Past question.
    def test_past_question(self):
        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_feature_question(self):
        create_question(question_text="Feature question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    # 質問リスト：Past question.(未来の質問は表示されない)
    def test_feature_question_and_past_question(self):
        create_question(question_text="Feature question.", days=30)
        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.>'])

    # 質問リスト：Past question1.、Past question2.
    # 質問リストは最新投稿順に取得されるため、作成順序と取得順序が弱になることに注意
    def test_tow_past_question(self):
        create_question(question_text="Past question1.", days=-30)
        create_question(question_text="Past question2.", days=-10)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(response.context['latest_question_list'], [
                                 '<Question: Past question2.>', '<Question: Past question1.>'])
```

```python

(poll-HcNSSqhc) C:\django\poll>python manage.py test polls
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
........
----------------------------------------------------------------------
Ran 8 tests in 0.063s

OK
Destroying test database for alias 'default'...

(poll-HcNSSqhc) C:\django\poll>
```
OKと表示されました。

## DetailView のテスト

detailビューも未来の日付の質問詳細が表示できる状態となっているので改修しましょう。

```python:polls/views.py

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

```python:polls/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)
```
```python

(poll-HcNSSqhc) C:\django\poll>python manage.py test polls
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..........
----------------------------------------------------------------------
Ran 10 tests in 0.088s

OK
Destroying test database for alias 'default'...

(poll-HcNSSqhc) C:\django\poll>
```

今日はここまでにします。ありがとうございました。