TDD with Python (3) - Chapter 3-1

第3章に突入。意図的に先頭の2章は軽めに作ってあると書いてあったので、ここからは更新のペースが落ちるはず。

Test-Driven Development with Python

$ python3 manage.py startapp lists
Kens-Macbook-Air-2010:superlists ken$ tree
.
├── Untitled.ipynb
├── db.sqlite3
├── functional_test.py
├── lists
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── manage.py
└── superlists
    ├── __init__.py
    ├── __pycache__
    │   ├── __init__.cpython-34.pyc
    │   ├── settings.cpython-34.pyc
    │   ├── urls.cpython-34.pyc
    │   └── wsgi.cpython-34.pyc
    ├── settings.py
    ├── urls.py
    └── wsgi.py

4 directories, 19 files

Djangoはプロジェクトの中に複数のappを構造化しますと。superlistsというプロジェクトを作ったときに、superlistsというappが作られたのに対して、startapp listsというのをmanage.pyのパラメータとして実行したことで、listsという新しいappが作られたと考えれば良いんだね。

Unit TestとFunctional Testの違い

ここで、Unit TestとFunctional Testの違いというのがblurry(曖昧)になるということで、少なくともこの本の中での棲み分けとして、Functional Testをユーザ目線、Unit Testはプログラマー目線というのが書いてある。前の章でコメントをひたすら書いたのがFunctional Testだという理解の前提であれば、Unit Testは作った部品がちゃんと動くかなという視点と考えれば大きな矛盾はないと思う。いずれにしても、多くの人が関わるプロジェクトの中では、言葉の定義を合わせておくというのは大事な事なのは確か。

上記を踏まえて、この本の進め方としては、こんな感じ。

  1. ユーザ観点で、新しい機能を記述するFunctional Testを書くところから始める
  2. Functional Testで失敗(Fail)したら、テストをパスするためにどのようなコードを書けば良いか考える。そこでUnit Testを使って、コードの1行1行がどのように動くべきかを定義する。コードのすべての行が少なくとも1つのUnit Testで確認されるようにするべき
  3. Unit TestでFailしたら、Unit Testをパスするための必要最低限のコードを書く(あれもこれもと欲張らなくて良いと理解)。2と3のステップを繰り返し、Functional Testで先に進める事が可能と判断できるまで続ける。
  4. Functional Testに戻り、先に進む。Failしたら、またUnit testを繰り返す。

Djangoを使ったUnit Test

ここで、さっき作ったlists appのディレクトリ階層の中に、test.pyというファイルがすでに出来ているので、ソースを見てみるともう何か書かれている。

from django.test import TestCase

# Create your tests here.

ここで、簡単なロジックを追加すると。

1+1=3になるとassertするんですね。それを実行すると、

$ python3 manage.py test
Creating test database for alias 'default'...
F
======================================================================
FAIL: test_bad_maths (lists.tests.SmokeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/ken/Documents/workspace/PycharmProjects/TDDPython/superlists/lists/tests.py", line 5, in test_bad_maths
    self.assertEqual(1 + 1, 3)
AssertionError: 2 != 3

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

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

これは意図したエラーだね。そうですね。ここでcommitすると。

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

	lists/

nothing added to commit but untracked files present (use "git add" to track)
$ git add lists
$ git diff --staged
...(中略)...
$ git commit -m"Add app for lists, with deliberately failing unit test" 
[master 93ee6be] Add app for lists, with deliberately failing unit test
 Committer: Ken <ken@Kens-Macbook-Air-2010.local>
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly. Run the
following command and follow the instructions in your editor to edit
your configuration file:

    git config --global --edit

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

 7 files changed, 20 insertions(+)
 create mode 100644 lists/__init__.py
 create mode 100644 lists/admin.py
 create mode 100644 lists/apps.py
 create mode 100644 lists/migrations/__init__.py
 create mode 100644 lists/models.py
 create mode 100644 lists/tests.py
 create mode 100644 lists/views.py
$

この本は、commitのタイミングまで適切に指示してくれます。

DjangoMVCモデル準拠?URLとView機能

DjangoはクラシックなMVCモデルに基づいている。モデル(Model)はあるけど、ビュー(View)はもっとコントローラっぽい。とにかく、MVCのコンセプト自体は息づいている。他のWebサーバーと同様にDjangoの役割は、特定のURLを外部から受け取って、どのような動作をするか決めることにあって、こんなワークフローになっているとのこと。

  1. HTTPリクエストがURLを指定してやってくる。
  2. Djangoはルールに基づいて、どのView機能を使用するかを決定する。
  3. View機能がHTTPリクエストを処理して、HTTPレスポンスを返す。

これをベースにルートにアクセスされたときの振る舞いを決めて、functional testをパスでできるようなHTMLを返すような実験をしましょうと。

$ python3 manage.py test
Creating test database for alias 'default'...
E
======================================================================
ERROR: lists.tests (unittest.loader.ModuleImportFailure)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", line 58, in testPartExecutor
    yield
  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", line 577, in run
    testMethod()
  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/loader.py", line 32, in testFailure
    raise exception
ImportError: Failed to import test module: lists.tests
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/loader.py", line 312, in _find_tests
    module = self._get_module_from_name(name)
  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/loader.py", line 290, in _get_module_from_name
    __import__(name)
  File "/Users/ken/Documents/workspace/PycharmProjects/TDDPython/superlists/lists/tests.py", line 3, in <module>
    from lists.views import home_page #1
ImportError: cannot import name 'home_page'


----------------------------------------------------------------------
Ran 1 test in 0.003s

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

そりゃ、怒られるわな。まだhome_pageなんていう関数作った覚えないし。じゃ、home_page作ればいいんでしょ?ということでこれを作る。(lists/view.py)

from django.shortcuts import render

# Create your views here.
home_page = None

それでやり直し。

$ python3 manage.py test
Creating test database for alias 'default'...
E
======================================================================
ERROR: test_root_url_solves_to_home_page_view (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/ken/Documents/workspace/PycharmProjects/TDDPython/superlists/lists/tests.py", line 7, in test_root_url_solves_to_home_page_view
    found = resolve('/')    #2
  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/django/core/urlresolvers.py", line 534, in resolve
    return get_resolver(urlconf).resolve(path)
  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/django/core/urlresolvers.py", line 404, in resolve
    raise Resolver404({'tried': tried, 'path': new_path})
django.core.urlresolvers.Resolver404: {'tried': [[<RegexURLResolver <RegexURLPattern list> (admin:admin) ^admin/>]], 'path': ''}

----------------------------------------------------------------------
Ran 1 test in 0.009s

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

何も割り当ててないんだから、ダメだろと思ったら予想通り。でも、エラー404的な文字が見えるのがさっきとは違うね。

url.py

url.pyというファイルで、与えられたURLに対してどのようにView functionを割り当てるのかというのを定義しているとのこと。これまで見ていたlistsの中ではなくて、superlists/superlistsの下にあるのがポイント。

adminは今回使わないのでコメントアウトして、viewsも少しいじってテストするとこうなる。

$ python3 manage.py test
Creating test database for alias 'default'...
/Users/ken/Documents/workspace/PycharmProjects/TDDPython/superlists/superlists/urls.py:22: RemovedInDjango110Warning: Support for string view arguments to url() is deprecated and will be removed in Django 1.10 (got lists.views.home_page). Pass the callable instead.
  url(r'^$', 'lists.views.home_page', name='home'),

.
----------------------------------------------------------------------
Ran 1 test in 0.005s

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

はい。OK。例によって、commitします。

$ git commit -am"First unit test and url mapping, dummy view"
[master 0dc4101] First unit test and url mapping, dummy view
 Committer: Ken <ken@Kens-Macbook-Air-2010.local>
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly. Run the
following command and follow the instructions in your editor to edit
your configuration file:

    git config --global --edit

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

 3 files changed, 10 insertions(+), 4 deletions(-)

(つづく)