Django+Reactで学ぶプログラミング基礎(11): Djangoチュートリアル(投票アプリその5-2)

##### [前回] [Django+Reactで学ぶプログラミング基礎(10): Djangoチュートリアル(投票アプリその5-1)](https://qiita.com/mingchun_zhao/items/d179c355cc185b73132b)

## はじめに

[Django公式チュートリアル](https://docs.djangoproject.com/ja/4.0/intro/tutorial01/)、その5-2です。
前回は、DjangoのAPIを駆使し、動作確認を行いました。
今回は、超重要な自動テストについてです。

## Djangoアプリ作成(その5-2): 投票(poll)アプリ

### 今回の内容

- 自動テスト

### 自動テストの導入

#### 自動テストとは
- テストは、コードの動作をチェックするルーチン
- 自動テストは、テスト作業がシステムによって実行されること(手動でなく)
    - 一度テストセットを作成すると、
        - アプリに変更を加えるたびに、
            - 意図した通りにコードが動作するか確認できる

#### なぜテストを作成する必要があるか
- ①テストはあなたの時間を節約
    - 高機能アプリでは、コンポーネント間の複雑な相互作用が多数存在
    - コンポーネントを変更した場合、動作確認のため様々なテストデータを用いてプログラムを走らせる必要あり
    - 自動テストを導入することで、プログラムの動作確認をを一瞬で終わらせることができる
        - プログラムのどこで予期せぬ動作が起きたかを見極めるのに役立つ
- ②問題点検出のみならず、問題発生を防ぐ
    - テストなくしては、アプリの目的や意図した動作が曖昧になってしまう
    - 自分自身で書いたコードであっても、コードがすることを正確に理解するのに時間がかかってしまうことがあり
    - テストにより、自分自身では気づかなかった間違いを見つけ出してくれる
- ③コードをより魅力的にする
    - テストをしていないアプリは信用できず、だれも使ってくれない
        - Djangoを開発した`Jacob Kaplan-Moss`氏より
            - `テストのないコードは、デザインとして壊れている`
    - ソフトウェアを他の開発者に真剣に見てもらうため
        - テストを書くべき
- ④チームの共同作業に役立つ
    - 複雑なアプリはチームでメンテナンスされる
    - テストは、誰かのうっかりミスによりコードを壊さないように守ってくれる
    - Djangoプログラマとして生きてゆくには、絶対に良いテストを書かなければならない

### 基本的なテスト方針とアプローチ

- テスト駆動開発
    - 実際にコードを書く前にテストを書く
    - 問題をきちんと言葉にしてから、その問題を解決するためのコードを書く
    - テスト駆動開発は、問題をPythonのテストケースとして形式化
- どこからテストを書き始めるべきか
    - 新しい機能追加やバグ修正を行う時

### 早速、テストを作成

#### バグ、みーつっけ

- `Question.was_published_recently()`メソッド
    - (OK)`Question`が昨日以降に作成された場合、`True`を返す
    - (NG)`Question`の`pub_date`が未来の日付になっている場合も、`True`を返す

- shellを使用し、メソッドのバグを再現確認
```
C:\kanban\pollsite>..\venv\.venv\Scripts\activate
(venv) C:\kanban\pollsite>python manage.py shell
```
- 必要モジュールをインポート
```
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
```

- `pub_date`値が今日から30日後となるように、`Question`インスタンスを作成 
```
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
```

- `was_published_recently()`メソッドを呼び出す
    - 未来の日付なのに、最近公開されたとうそを言っている
```
>>> future_question.was_published_recently()
True
>>> exit()
```

### バグをあぶり出すため、テストを作成

- 上述の手動によるバグ確認を、自動テストに変換
- アプリケーションのテストを書く場所
    - `tests.py`ファイル
    - テストシステムが、名前が`test`で始まるファイルから、自動的にテストを見つけてくれる
- `django.test.TestCase`を継承したサブクラスを作成
    - 未来の日付の`pub_date`を持つ`Question`のインスタンスを生成するメソッドを作成
    - `was_published_recently()`の出力をチェック
        - 期待値`False`と一致するか
```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):
        """
        was_published_recently() returns False for questions whose pub_date
        is in the future.
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/ccd07535-8945-9905-cbc0-64aca7a5444e.png)

- `polls/tests.py`を保存

### テストを実行
```
(venv) C:\kanban\pollsite>python manage.py test polls

Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
was_published_recently() returns False for questions whose pub_date
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\kanban\pollsite\polls\tests.py", line 18, in test_was_published_recently_with_future_question
    self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False
```

- テストが失敗し、失敗が起こったコードの行数まで教えてくれる
- 上記テストでやっていること
    - `manage.py test polls`は、pollsアプリ内にあるテストを探す
    - `django.test.TestCase`クラスのサブクラスを発見
    - テスト用のデータベースを作成
    - `test`で始まるテスト用メソッドを探す
    - `test_was_published_recently_with_future_question`の中で
        - pub_dateフィールドが、今日から30日後の日付となる`Question`インスタンスを作成
    - `assertIs()`メソッドを使用し、以下二つが一致するか評価
        - 期待値: `False`
        - `was_published_recently()`の返却値: `True`

### バグを修正

- 問題の原因
    - `pub_date`が未来の日付の場合
        - `Question.was_published_recently()`メソッドが、間違って`True`を返している(正しくは`False`)
        - メソッドを修正し、日付が過去だった場合にのみ`True`を返すように
```polls/models.py
def was_published_recently(self):
    now = timezone.now()
    return now - datetime.timedelta(days=1) <= self.pub_date <= now
```

### 再度、テストを実行
```
(venv) C:\kanban\pollsite>python manage.py test polls
Found 1 test(s).
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'...
```

バグを修正したので、テストが正常に終わりました。

### より包括的なテスト

- 一つのバグを修正する際、別のバグを作り出す可能性あり
    - 対処: メソッドの振る舞いをより包括的にテスト
        - 同じクラスにさらに2つのテストを追加

```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)
```

- これで、`Question.was_published_recently()`が、考えられるすべてのケースに対し、チェック実施
    - 過去質問
    - 現在質問
    - 未来質問
- メソッドに対し、テストを書いたおかげで
    - 将来、このアプリがどんなに複雑になっても
    - または、他のどんなコードと相互作用するようになっても
        - メソッドが期待どおり動作することを保証できる

## おわりに

自動テストがもたらすメリットを理解しました。
テストの作成、本当に大事ですね。
次回も続きます。お楽しみに。

##### [次回] [Django+Reactで学ぶプログラミング基礎(12): Djangoチュートリアル(投票アプリその5-3)](https://qiita.com/mingchun_zhao/items/9528be81b614d28eafa2)