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は作った部品がちゃんと動くかなという視点と考えれば大きな矛盾はないと思う。いずれにしても、多くの人が関わるプロジェクトの中では、言葉の定義を合わせておくというのは大事な事なのは確か。
上記を踏まえて、この本の進め方としては、こんな感じ。
- ユーザ観点で、新しい機能を記述するFunctional Testを書くところから始める
- Functional Testで失敗(Fail)したら、テストをパスするためにどのようなコードを書けば良いか考える。そこでUnit Testを使って、コードの1行1行がどのように動くべきかを定義する。コードのすべての行が少なくとも1つのUnit Testで確認されるようにするべき
- Unit TestでFailしたら、Unit Testをパスするための必要最低限のコードを書く(あれもこれもと欲張らなくて良いと理解)。2と3のステップを繰り返し、Functional Testで先に進める事が可能と判断できるまで続ける。
- 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のタイミングまで適切に指示してくれます。
DjangoはMVCモデル準拠?URLとView機能
DjangoはクラシックなMVCモデルに基づいている。モデル(Model)はあるけど、ビュー(View)はもっとコントローラっぽい。とにかく、MVCのコンセプト自体は息づいている。他のWebサーバーと同様にDjangoの役割は、特定のURLを外部から受け取って、どのような動作をするか決めることにあって、こんなワークフローになっているとのこと。
- HTTPリクエストがURLを指定してやってくる。
- Djangoはルールに基づいて、どのView機能を使用するかを決定する。
- 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(-)
(つづく)