Djangoを始めよう！ 〜チュートリアル⑤〜

# 概要

全4回に渡って、Djangoのチュートリアルを解説してきましたが、
[前回](http://qiita.com/gcfuji/items/68101227141cde134326)で一通りのアプリは完成しました。

第5回は自動テストの導入について解説していきます。

# 自動テストとは

テストとは、コードの動作を確認する単純なプログラムです。

テストは他のアプリのコードとは異なるレベルで実行されます。あるテストは、小さな機能に対して行われるもの (ある特定のモデルのメソッドは期待通りの値を返すか？) かもしれませんし、別のテストは、ソフトウェア全体の動作に対して行われるもの (サイト上でのユーザの一連の入力に対して、期待通りの結果が表示されるか？) かもしれません。

自動テストが他と異なる点は、テスト作業がシステムによって実行されることです。一度テストセットを作成すると、それからはアプリに変更を加えるたびに、あなたの意図した通りにコードが動作するか確認できます。手動でテストする時間がかかることはありません。

## テストコードがないプロジェクト

テストのコードがあることで、GitHubなどでも人が見ても問題ないという安心感を与える事ができます。
(なければ、このプロジェクト大丈夫ってなる。)

そのテストのコードを実行して、Suceessを返せば、例えどんな修正を入れていても、バグはなくすぐにローンチする事が可能だからです。

テストコードを書くことは、ただただテストするというだけではなく、
コードを読む人に安心感を与えること、複数人で開発した時に、一連のテストは担保できることを保証してくれます。

「テスト駆動型」の原則に従って書け！とまでは言いませんが、最終的にテストを準備する事は絶対不可欠です。

# 初めてのテストの作成

Djangoでも、テストコードを作成することを推奨しており、
テストを簡単に作成できるモジュールが準備されています。

実際に1つの例を挙げて、テストコードを実行してみたいと思います。

実は、前回までで作成した投票アプリには1つの小さなバグがあります。

```py3:polls/models.py
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils import timezone
import datetime

# Create your models here.


@python_2_unicode_compatible
class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __str__(self):
        return self.question_text

	# このメソッド
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)


@python_2_unicode_compatible
class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __str__(self):
        return self.choice_text
```

上記の、```was_published_recently```メソッドは、Questionが昨日以降に作成された場合にTrueを返すメソッドです。

それだけならOKなのですが、このメソッドは未来日付でもTrueを返してしまいます。
(そんな投票あるわけない。)

実際に、Djangoが提供するshellコマンドで動作を確認してみましょう。

ターミナルで以下のコマンドを実行します。

```shell_session
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # create a Question instance with pub_date 30 days in the future
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # was it published recently?
>>> future_question.was_published_recently()
True
```

やはり予想通りTureが返ってきてしまいました。
未来日付は、'最近'ではない為、この結果は間違っているといえます。

上記でやった事を、テストコードとしてプロジェクトに残し、いつでもテスト出来たら便利ですよね。
(そのメソッドが何をしたいかもわかる。)

実際にテストコードを作成します。

まず、テストコードを配置するディレクトリ構成ですが、
pollsディレクトリ以下のtests.pyという名称で作成します。

```py3:polls/tests.py
from django.test import TestCase

import datetime
from django.utils import timezone

from .models import Question

# Create your tests here.


class QuestionMethodTest(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)
```

ここではまず、django.test.TestCase を継承したサブクラスを作り、未来の日付の pub_date を持つ Question のインスタンスを作っています。それから、was_published_recently() の出力をチェックしています。これは False になるはずです。

すごく直感的なコードで、Python/Djangoらしいですね。

# テストの実行

ターミナルで以下のコマンドを実行すれば、自動テストが開始されます。

```
$ python manage.py test polls
```

すると、以下の実行結果になります。

```
Creating test database for alias 'default'...
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests)
----------------------------------------------------------------------
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'...
```

上記は、以下の様な事が行われています。

- python manage.py test polls は polls アプリケーションからテストを探します
- django.test.TestCase クラスのサブクラスを発見します
- テストのための特別なデータベースを作成します
- テスト用のメソッドとして、test で始まるメソッドを探します
- test_was_published_recently_with_future_question の中で、pub_date フィールドに今日から30日後の日付を持つ Question インスタンスが作成されます
- assetIsメソッドの引数の結果が正しいかを判定します。

# バグを修正する

実際に、Question.was_published_recentlyを修正しましょう。

```py3:polls/models.py
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils import timezone
import datetime

# Create your models here.


@python_2_unicode_compatible
class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __str__(self):
        return self.question_text
	
	# 修正したメソッド
    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now


@python_2_unicode_compatible
class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __str__(self):
        return self.choice_text
```

修正後、再度テストコマンドを実行します。

```
Creating test database for alias 'default'....
----------------------------------------------------------------------
Ran 1 test in 0.001s

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

上記の様にテストが無事完了してOKと表示されました。

# まとめ

今回の自動テストは、モデルのテストをメインで行いましたが、もちろんビューやその他のテストも存在します。

ざっくりとテストの全容と、テストコードの書き始めのみを紹介しました。

違う機会で、他のテストコードを書く方法も紹介できたらと思います。

次回は、最終章でDjangoでの静的ファイルの取り扱いについて解説していきたいと思います。

[GitHub](https://github.com/gcfuji/django_tutorial/tree/tutorail-5)

# シリーズ

- [Djangoを始めよう！ 〜チュートリアル①〜](http://qiita.com/gcfuji/items/bff55a0b8ae1b76c0ca1)
- [Djangoを始めよう！ 〜チュートリアル②〜](http://qiita.com/gcfuji/items/27d2c9e4b3334447b6af)
- [Djangoを始めよう！ 〜チュートリアル③〜](http://qiita.com/gcfuji/items/c8a0e4add7a628c12f8d)
- [Djangoを始めよう！ 〜チュートリアル④〜](http://qiita.com/gcfuji/items/68101227141cde134326)
- [Djangoを始めよう！ 〜チュートリアル⑤〜](http://qiita.com/gcfuji/items/1b6079bec064a4e8b211)
- [Djangoを始めよう！ 〜チュートリアル⑥〜](http://qiita.com/gcfuji/items/8560eade5a11c08a0ff1)