前回は、テスト駆動開発のための環境設定を主に行いました。
今回から、テスト駆動開発を通して、ToDoアプリケーションを開発していきます。
前回
Selenium実行
Seleniumの実行だけ復習しておきましょう。
下記のコードは、前回実装したコードに「browser.quit()」を追加したものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# functional_tests.py from selenium import webdriver # Selenium Driverを通して、ブラウザの動作を可能にする browser = webdriver.Chrome() # ブラウザを開く browser.get('http://localhost:8000') # ブラウザのタイトルがDjangoか確認する assert 'Django' in browser.title # ブラウザを閉じる browser.quit() |
terminalを2つ立ち上げ、以下のコマンドを実行しましょう。
1 2 3 4 5 |
# terminal 1 python manage.py runserver # terminal 2 pytest |
terminalが一瞬立ち上がって、消えたら成功です。
初めてのテスト関数
functional_tests.pyを関数化しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# functional_tests.py from selenium import webdriver URL = 'http://localhost:8000' class NewVisitorTest(object): def setup_method(self): self.browser = webdriver.Chrome() self.browser.get(URL) def teardown_method(self): self.browser.quit() def test_can_start_a_list_and_retrieve_it_later(self): assert 'Django' in self.browser.title |
setup_methodには、テスト前に実行したい処理を書きます。また、teardown_methodには、テスト終了後に実行したい処理を書きます。
これらのメソッドは、pytest特有のメソッドです。unittestでは、以下の様に書きます。
1 2 3 4 5 6 |
import unittest class NewVisitorTest(unittest.TestCase): def setUp(self): pass def tearDown(self): pass |
テストを実行しましょう。python manage.py runserverを実行していることを確認してから、pytestを実行してください。
1 2 3 |
$ pytest ============= 1 passed in 2.69s ================ |
OKですね。なお、pytestには、便利なオプションが多数ありますが、今はpytestコマンドだけで十分でしょう。
listsアプリケーションの追加
さて、Djangoのアプリケーションを追加しましょう。次のコマンドを実行してください。
1 |
python manage.py startapp lists |
上記のコマンドにより、「lists」アプリケーションを追加しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
$ tree -L 2 . ├── db.sqlite3 ├── functional_tests.py ├── lists # new │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── models.py │ ├── tests.py │ └── views.py ├── manage.py ├── pytest.ini ├── todos │ ├── __init__.py │ ├── __pycache__ │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── venv |
初めてのUnit Test
では、新しく作成したlistsアプリケーションにUnit Testを追加しましょう!
Djangoは、MVCタイプのフレームワークです。※Model-View-Templateなので、MVTとも呼ばれています。
以下、簡単な処理の流れです。
- ブラウザから特定のURLに対してリクエストを送る
- Viewに記述されたURLに紐付く関数により、リクエストが処理される
- HTTP responseをブラウザに返す
そのため、URLに紐付く関数が適切に処理されているか、適切なテンプレート(HTML)がブラウザに返却させているか、などをテストしていくことにしましょう。
まずは、「localhost:8000/」にアクセスした時、home_page関数が呼ばれるかテストします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# lists/tests.py from django.urls import resolve from django.test import TestCase from lists.views import home_page class HomePageTest(TestCase): def test_resolve_url_to_home_page(self): """home_page関数のURLディスパッチャを確認 """ found = resolve('/') assert found.func == home_page |
resolve関数を使用するとブラウザ上で「localhost:8000/」にアクセスしたことと同じ処理を実行することができます。
テストを実行しましょう。
1 2 3 |
$ pytest lists/ E ImportError: cannot import name 'home_page' from 'lists.views' |
home_page関数を設定していないので、エラーになりました。
lists/views.pyに以下の関数を設定しましょう。
1 2 3 4 5 6 7 |
# lists/views.py from django.shortcuts import render def home_page(request): pass |
テストを実行してみましょう。
1 2 3 |
pytest E django.urls.exceptions.Resolver404: {'tried': [[<URLResolver <URLPattern list> (admin:admin) 'admin/'>]], 'path': ''} |
該当するURLが見つからないため、エラーになってしまいました。
urls.pyの追加
ではどうするのかというと、DjangoではURLを設定する専用のファイルであるurls.pyを用意します。
1 |
touch lists/urls.py |
1 2 3 4 5 6 7 8 9 10 11 |
# lists/urls.py from django.urls import path from .views import home_page app_name = 'lists' urlpatterns = [ path('', home_page, name='home'), ] |
これで、ブラウザからのリクエストをhome_page関数へ渡すことができます。
それでは、テストを実行しましょう!
1 2 3 4 5 6 |
pytest E django.urls.exceptions.Resolver404: {'tried': [[<URLResolver <URLPattern list> (admin:admin) 'admin/'>]], 'path': ''} venv/lib/python3.7/site-packages/django/urls/resolvers.py:575: Resolver404 |
はい!エラーになりました。実は、まだ設定が足りないんですよ。
urls.pyには、プロジェクト(todos)とアプリ(lists)の両方に存在していて、先ほど設定したurls.pyは、アプリ側の設定なのです。
しかし、Djangoは、起動してから最初にプロジェクトのurls.pyを見にいくので、そちら側にも設定する必要があります。
1 2 3 4 5 6 7 8 9 |
# todos/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('lists.urls')) ] |
テストを実行してみましょう。
1 2 3 |
pytest lists/ === 1 passed in 0.29s === |
テストがパスしました!
まぁぶっちゃけ、URLテストはなくてもいいかなと個人的には思ってます(笑)。めんどくさいですよね。
テスト駆動はこんな感じなので、しょうがないです。
2. テストを実行し、「エラーになること」を確認する
3. テストがpassするように「必要最低限」のプロダクトコードを書く
4. テストを実行し、「passすること」を確認する
5. リファクタリングする
ただ、バグはめっちゃ減ります。堅牢なシステムの出来上がりです。トレードオフなんですよね。
Viewのテスト
ブラウザのリクエストから目的の関数を実行できること確認するテストをpassしたので、次はView自体のテストに着手しましょう。
home_page関数では、ホニャララな処理ののちに、HTMLを返します。ホニャララな部分については後ほど考えるとして、まずはHTMLを返すようにしてみましょう。
まずは、テストからです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# lists/tests.py from django.urls import resolve from django.test import TestCase from django.http import HttpResponse from lists.views import home_page class HomePageTest(TestCase): def test_resolve_url_to_home_page(self): """home_page関数のURLディスパッチャを確認 """ ... def test_home_page_retuns_expected_html(self): """期待したHTMLが返却されるかテストする """ # Init request = HttpResponse() # Execution response = home_page(request) html = response.content.decode('utf8') # Validation assert html.startswith('<html>') assert '<title>To-Do lists</title>' in html assert '<body>Hello Our To-Do Lists!</body>' in html assert html.endswith('</html>') |
上記のテストでは、home_page関数を実行したら期待したhtml要素を返却されるか確認しています。
テストを実行しましょう。
1 2 3 4 |
$ pytest lists/ E AttributeError: 'NoneType' object has no attribute 'content' ===== 1 failed, 1 passed in 0.39s ===== |
テストが失敗しましたね(計算通りです)!
プロダクトコードを修正します。
1 2 3 4 5 6 7 |
# lists/views.py from django.http import HttpResponse def home_page(request): return HttpResponse() |
テストを実行します。
1 2 3 |
$ pytest lists/ E AssertionError: assert False |
エラー内容が変わりましたね。続きを実装しましょう。
1 2 3 4 5 6 7 |
# lists/views.py from django.http import HttpResponse def home_page(request): return HttpResponse('<html><title>To-Do lists</title><body>Hello Our To-Do Lists!</body></html>') |
テストを実行します。
1 2 3 |
$ pytest lists/ ==== 2 passed in 0.30s ===== |
テストがパスしました!
localhost:8000にアクセスしてみましょう。

OKですね。
functional_tests.pyも実行してみましょう。
1 2 3 |
$ pytest functional_tests.py E assert 'Django' in 'To-Do lists' |
テストが失敗しましたね。原因は、HTMLのタイトルを変更したためなので、サクッと直してしまいましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# functional_tests.py from selenium import webdriver URL = 'http://localhost:8000' class NewVisitorTest(object): def setup_method(self): self.browser = webdriver.Chrome() self.browser.get(URL) def teardown_method(self): self.browser.quit() def test_can_start_a_list_and_retrieve_it_later(self): assert 'To-Do lists' in self.browser.title # 修正 |
「test_can_start_a_list_and_retrieve_it_later」のassertを「Django」 -> 「To-Do lists」に変更しました。
テストを実行します。
1 2 3 |
$ pytest functional_tests.py ===== 1 passed in 2.88s ====== |
OKですね。
今回は、ここまでにしましょう。
まとめ
今回は、URLとViewに関するちょっとしたテスト関数を実装し、テストを行いました。
まだまだテスト駆動開発の道のりは遠く険しいですが、なんとなくやり方はご理解されたのかな?と勝手に思っています^^
いやぁ、テストの実行結果をブログに書き写していく作業って、結構大変なんですよね。
それと同時に、「こんなにチマチマテストしてないで、サクッとプロダクトコードを書いて、仕事を終えて〜〜」っていう衝動にかられたりします。
でも、ここはグッっとこらえて、私と一緒にテスト駆動開発を勉強していきましょう!
それでは、また!
コメントを残す
コメントを投稿するにはログインしてください。