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

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

## はじめに

[Django公式チュートリアル](https://docs.djangoproject.com/ja/4.0/intro/tutorial01/)、その5-4です。
前回は、Djangoテストクライアントを使用し、ビューをテストしました。
今回は、その続きで、`DetailView`ビューをテストします。

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

### 今回の内容

- `DetailView`ビューをテスト
- テスト作成のベストプラクティス
- テストの総括

### `DetailView`ビューをテスト

- 理論: テストの作成に、どこから着手すればよいか
    - まず、ユーザーがどのようにアプリを使用するか、ストーリーを考える
        - 管理者の入力などの操作
        - ユーザがサイトでの操作
    - つぎ、システムの各状態と新しい変化に対し、期待通りの結果が得られるかをチェック

- `DetailView`ビューの問題点
    - 未来の質問は表示されなくなったが、正しいURLを推測できたら、勝手にアクセスできてしまう
    - `DetailView`に制約を追加
```polls/views.py
class DetailView(generic.DetailView):
    ...
    def get_queryset(self):
        """
        Excludes any questions that aren't published yet.
        """
        return Question.objects.filter(pub_date__lte=timezone.now())
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/3aadbe59-70a0-ada8-1ff2-fcc77b641d3d.png)

- テストを追加し、以下をチェック
    - `pub_date`が過去となっている質問は表示されること
    - `pub_date`が未来となっている質問は表示されないこと
```polls/tests.py
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)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/4c3a396c-0f13-b102-8e51-6109cff332d7.png)

- 作成したテストを実行
```
C:\kanban\pollsite>..\venv\.venv\Scripts\activate
(venv) C:\kanban\pollsite>python manage.py test polls
Found 10 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..........
----------------------------------------------------------------------
Ran 10 tests in 0.135s

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

テスト結果、すべてのケースがOKとなりました。

### さらなるテストについて考える

- 懸念1: 異なるビューでテストが重複してもよいか
    - `ResultsView`も、上記と同じく`get_queryset`メソッドを追加する必要あり
    - よって、ビュー毎に同じテストがたくさん作られてしまう

- 懸念2: アプリに新しく追加されたコードは、必ずテストコードが伴う
    - テストが膨らみすぎて、コードが膨大な量にならないか
    - テストコードがアプリのコードよりも大きくなり
    - コードのエレガントさや簡潔さが失われるのではないか

- 心配無用、テストは多ければ多いほどよい
    - テストを一回書いたら、繰り返し便利に機能し続ける

- テストのアップデートが必要になったら
    - たとえば、選択肢を持つ質問だけ公開するようにビューを修正したら
        - 既存テストの多くは失敗する
    - ただし、この失敗によって、最新状態に対応するためには、どのテストを修正すべきか正確に把握できる
    - つまり、テストにより、テスト自身もチェック可能に
- 過去のテストが冗長になってきたら
    - テストにおいて、冗長であることは`良い`こと
    - きちんとテストを整理していれば、テストが手に負えなくなることはない

### テスト作成のベストプラクティス

- モデルやビューごとに`TestClass`を分割
- テスト条件の集まりそれぞれに、異なるテストメソッドを作る
- テストメソッドの名前は、その機能を説明できるものにする

### テストの総括

#### チュートリアルでテスト対象にしたのは
- モデルの内部ロジック
- ビューの情報表示の仕方

#### 他に、ブラウザが`HTML`を正しくレンダリングするかのテストも存在
- `Selenium`のような`in-browser`フレームワークなど、テストツールを使用
- これらのツールは、Djangoコードの振る舞いだけでなく、JavaScriptの振る舞いも確認できる
- テストがユーザーに代わりに、ブラウザ起動し、サイトを操作しながらチェックしてくれる
    - まるで本物の人間がブラウザを操作しているかのように見えて面白い
- Djangoには、`Selenium`のようなツールとの連携を容易にしてくれる`LiveServerTestCase`が用意されている

#### 複雑なアプリケーション開発時
- コミットの度に自動テストを実行し、CI(継続的インテグレーション、continuous integration)を実現
    - CIにより、品質管理の自動化にもつながる
- コードカバレッジをチェック
    - アプリケーションでテストされていない部分を発見
    - 壊れやすいコードや使用されていないデッドコードを発見
- テストできないコードはどうするか
    - そのコードはリファクタリングするか削除する必要がある
    - カバレッジはデッドコードの識別にも役立つ

## おわりに

4回にわたって、テストのトピックを取り上げました。
そこからも、テストの重要性がうかがえます。

次回も続きます。お楽しみに。

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