LoginSignup
1
3

More than 3 years have passed since last update.

Djangoでテスト駆動開発 その5

Posted at

Djangoでテスト駆動開発 その5

これはDjangoでテスト駆動開発(Test Driven Development, 以下:TDD)を理解するための学習用メモです。

参考文献はTest-Driven Development with Python: Obey the Testing Goat: Using Django, Selenium, and JavaScript (English Edition) 2nd Editionを元に学習を進めていきます。

本書ではDjango1.1系とFireFoxを使って機能テスト等を実施していますが、今回はDjagno3系とGoogle Chromeで機能テストを実施していきます。また一部、個人的な改造を行っていますが(Project名をConfigに変えるなど、、)、大きな変更はありません。

⇒⇒その1 - Chapter1はこちら
⇒⇒その2 - Chapter2はこちら
⇒⇒その3 - Chapter3はこちら
⇒⇒その4 - Chapter4はこちら

Part1. The Basics of TDD and Django

Chapter5. Saving User Input: Testing the Database

ユーザーからのインプットがある場合を想定してテスト駆動開発をしていきましょう。

Wiring Up Our Form to Send a Post Request

ユーザーからインプットがある場合、それが問題なく保存されていることを確認するテストが必要です。
標準的なHTMLからのPOSTリクエストを想定してみます。
home.htmlにformタグを挿入して見ましょう。

<!-- lists/home.html -->
<html>
    <head>
        <title>To-Do lists</title>
    </head>
    <body>
        <h1>Your To-Do list</h1>
            <form method="post">
                <input name="item_text" id="id_new_item" placeholder="Enter a to-do item">
            </form>
        <table id="id_list_table">
        </table>
    </body>
</html>

機能テストを実行してみましょう。

$ python functional_tests.py


DevTools listening on ws://127.0.0.1:58348/devtools/browser/2ec9655c-1dd9-4369-a97b-fb7099978b93
E
======================================================================
ERROR: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "functional_tests.py", line 42, in test_can_start_a_list_and_retrieve_it_later
    table = self.browser.find_element_by_id('id_list_table')
  File "C:--your_path--\django-TDD\venv-tdd\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 360, in find_element_by_id
    return self.find_element(by=By.ID, value=id_)
  File "C:--your_path--\django-TDD\venv-tdd\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 978, in find_element
    'value': value})['value']
  File "C:--your_path--\django-TDD\venv-tdd\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 321, in execute
    self.error_handler.check_response(response)
  File "C:--your_path--\django-TDD\venv-tdd\lib\site-packages\selenium\webdriver\remote\errorhandler.py", line 242, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="id_list_table"]"}
  (Session info: chrome=79.0.3945.130)


----------------------------------------------------------------------
Ran 1 test in 9.982s

FAILED (errors=1)

機能テストが予想していない内容で失敗しました。
Seleniumによる実行画面を確認してみるとcsrf_tokenによるアクセスエラーでした。
Djangoテンプレートタグを使ってCSRFタグを追加しましょう。

<!-- lists/home.html -->
<html>
    <head>
        <title>To-Do lists</title>
    </head>
    <body>
        <h1>Your To-Do list</h1>
            <form method="post">
                <input name="item_text" id="id_new_item" placeholder="Enter a to-do item">
                {% csrf_token %}
            </form>
        <table id="id_list_table">
        </table>
    </body>
</html>

機能テストを実施します。

DevTools listening on ws://127.0.0.1:58515/devtools/browser/0bbd1ede-a958-4371-9d8a-99b5cd55b9c3
F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "functional_tests.py", line 46, in test_can_start_a_list_and_retrieve_it_later
    "New to-do item did not appear in table"
AssertionError: False is not true : New to-do item did not appear in table

----------------------------------------------------------------------
Ran 1 test in 13.718s

FAILED (failures=1)

エラー内容がNew to-do item did not appear in tableとなりました。
これはまだサーバー側がPOSTリクエストの処理を追加していないためです。

Processing a POST Request on the Server

home_page View にPOST処理を追加していきましょう。その前にlist/tests.pyを開いてHomePageTestにPOSTが保存されているかどうかを確認するテストを追加します。

# lists/tests.py

from django.test import TestCase


class HomePageTest(TestCase):

    def test_users_home_template(self):
        response = self.client.get('/')  # URLの解決
        self.assertTemplateUsed(response, 'lists/home.html')

    def test_can_save_a_POST_request(self):
        response = self.client.post('/', data={'item_text': "A new list item"})
        self.assertIn('A new list item', response.content.decode())

ここで現在のlists/views.pyを確認しておきます。

# lists/views.py

from django.shortcuts import render


def home_page(request):
    return render(request, 'lists/home.html')

現在のViewはリクエストに対してhome.htmlを返すのみとなっています。
単体テストを行ってみましょう。

python manage.py test

======================================================================
FAIL: test_can_save_a_POST_request (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:--your_path--\django-TDD\lists\tests.py", line 14, in test_can_save_a_POST_request
    self.assertIn('A new list item', response.content.decode())
AssertionError: 'A new list item' not found in '<!-- lists/home.html -->\n<html>\n    <head>\n        <title>To-Do lists</title>\n    </head>\n    <body>\n        <h1>Your To-Do list</h1>\n            <form method="post">\n                <input name="item_text" id="id_new_item" placeholder="Enter a to-do item">\n                <input type="hidden" name="csrfmiddlewaretoken" value="WxS3OX6BLnYLQuT4saIKUU4O18Z9mDZDIfhIrUiBjeeizFlf2ajmbj86QyzEo04R">\n            </form>\n        <table id="id_list_table">\n        </table>\n    </body>\n</html>\n'

----------------------------------------------------------------------

テスト結果を確認するとresponse.content.decode()の中身が表示されているのが分かります。
中身を確認すると、{% csrf_token %}により<input type="hidden">という形でCSRFトークンがDjangoによって追加されていることがわかります。

テストをパスするため、POSTで投げられているitem_textを返せるようにViewを変更します。

# lists/views.py

from django.shortcuts import HttpResponse  # 追加
from django.shortcuts import render


def home_page(request):
    if request.method == 'POST':
        return HttpResponse(request.POST['item_text'])
    return render(request, 'lists/home.html')

これで単体テストはパスしましたが、これは本当に行いたいことではありません。

Passing Python Variables to Be Rendered in the Template

Djagnoテンプレートでは{{ }}を使ってViewからの変数を利用することが可能です。
これを使ってPOSTさた内容を表示できるようにhome.htmlに追加しておきましょう。

    <body>
        <h1>Your To-Do list</h1>
            <form method="post">
                <input name="item_text" id="id_new_item" placeholder="Enter a to-do item">
                {% csrf_token %}
            </form>
        <table id="id_list_table">
            <tr><td>{{ new_item_text }}</td></tr>
        </table>
    </body>

Viewでnew_item_textを返すことでhome.htmlに表示させることができますが、そもそもリクエストに対してhome.htmlを返しているかどうかもテストしておかなければなりません。単体テストに追加しましょう。

def test_can_save_a_POST_request(self):
    response = self.client.post('/', data={'item_text': "A new list item"})
    self.assertIn('A new list item', response.content.decode())
    self.assertTemplateUsed(response, 'lists/home.html')  # 追加

単体テストを実行するとAssertionError: No templates used to render the responseとなりました。
これはPOST時のレスポンスにHTMLを指定していないためです。Viewを変更しましょう。

def home_page(request):
    if request.method == 'POST':
        return render(request, 'lists/home.html', {
            'new_item_text': request.POST['item_text'],
        })
    return render(request, 'lists/home.html')

これで単体テストはパスできました。それでは機能テストを実行してみます。

$ python functional_tests.py

======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "functional_tests.py", line 46, in test_can_start_a_list_and_retrieve_it_later
    "New to-do item did not appear in table"
AssertionError: False is not true : New to-do item did not appear in table

----------------------------------------------------------------------

アイテムを追加して表示できているはずにも関わらず、AssertionError: False is not true : New to-do item did not appear in tableとなりました。
エラー内容をより詳細に確認するために、機能テストの一部を書き換えて実行してみます。


self.assertTrue(
    any(row.text == "1: Buy dorayaki" for row in rows),
    f"New to-do item did not appear in table. Contents were: \
    \n{table.text}"
)

もう一度実行します。

$ python functional_tests.py

======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "functional_tests.py", line 47, in test_can_start_a_list_and_retrieve_it_later
    \n{table.text}"
AssertionError: False is not true : New to-do item did not appear in table. Contents were:
Buy dorayaki

----------------------------------------------------------------------

実際のtableのテキストを確認することができました。
これを見ると"1: Buy dorayaki"となるべきところがBuy dorayakiとなっているためにエラーが出ているようです。

これを解決するViewはまだ後になりそうです。今回はitem_textが返ってきているのかどうかを確認するためなので、機能テストをそのように書き換えます。

# functional_tests.py
[...]
self.assertIn('1: Buy dorayaki', [row.text for row in rows])
[...]

機能テストを実行します。

$ python functional_tests.py

======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "functional_tests.py", line 50, in test_can_start_a_list_and_retrieve_it_later
    self.assertIn('1: Buy dorayaki', [row.text for row in rows])
AssertionError: '1: Buy dorayaki' not found in ['Buy dorayaki']

----------------------------------------------------------------------
Ran 1 test in 8.901s

やはりエラーが出てしまいました。ポイントは追加されたアイテムを数え上げることですが、
機能テストをパスする最速の方法は1:を追加することです。

<table id="id_list_table">
    <tr><td>1: {{ new_item_text }}</td></tr>
</table>

これで機能テストを実行するとself.fail("Finish the test!")となり、機能テストは(とりあえずは)実行できました。

機能テストがパスできたので、新たに機能テストを更新します。

# django-tdd/functional_tests.py

[...]

# テキストボックスは引続きアイテムを記入することができるので、
# 「どら焼きのお金を請求すること」を記入した(彼はお金にはきっちりしている)
inputbox = self.browser.find_element_by_id('id_new_item')
inputbox.send_keys("Demand payment for the dorayaki")
inputbox.send_keys(Keys.ENTER)
time.sleep(1)

# ページは再び更新され、新しいアイテムが追加されていることが確認できた
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertIn('1: Buy dorayaki', [row.text for row in rows])
self.assertIn('2: Demand payment for the dorayaki',
[row.text for row in rows])

# のび太はこのto-doアプリが自分のアイテムをきちんと記録されているのかどうかが気になり、
# URLを確認すると、URLはのび太のために特定のURLであるらしいことがわかった
self.fail("Finish the test!")

# のび太は一度確認した特定のURLにアクセスしてみたところ、

# アイテムが保存されていたので満足して眠りについた。

[...]

これを実行してみましょう。

$ python functional_tests.py

======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "functional_tests.py", line 56, in test_can_start_a_list_and_retrieve_it_later
    self.assertIn('1: Buy dorayaki', [row.text for row in rows])
AssertionError: '1: Buy dorayaki' not found in ['1: Demand payment for the dorayaki']

----------------------------------------------------------------------
Ran 1 test in 13.815s

やはりid_list_tableにおいてitemを数え上げられていないことが原因で機能テストがパスできないようです。

機能テストを更新したのでコミットしておきます。

$ git add .
$ git commit -m "post request returns id_list_table"

機能テストにおける「テキストがあるかどうか」の判断は切り分けて処理を行うのが賢いやり方です。
機能テストをリファクタリングしましょう。

# functional_tests.py

[...]
def tearDown(self):
    self.browser.quit()

def check_for_row_in_list_table(self, row_text):
    table = self.browser.find_element_by_id('id_list_table')
    rows = table.find_elements_by_tag_name('tr')
    self.assertIn(row_text, [row.text for row in rows])

[...]

        # のび太がエンターを押すと、ページは更新され、
        # "1: どら焼きを買うこと"がto-doリストにアイテムとして追加されていることがわかった
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)  # ページ更新を待つ。
        self.check_for_row_in_list_table('1: Buy dorayaki')

        # テキストボックスは引続きアイテムを記入することができるので、
        # 「どら焼きのお金を請求すること」を記入した(彼はお金にはきっちりしている)
        inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox.send_keys("Demand payment for the dorayaki")
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)

        # ページは再び更新され、新しいアイテムが追加されていることが確認できた
        self.check_for_row_in_list_table('2: Demand payment for the dorayaki')

[...]

The Django ORM and Our First Model

DjangoのORM(Object-Relational Mapper)をつかってItemを追加するためのテーブルを作成していきましょう。
これはlists/models.pyに記述していきますが、先に単体テストを書いていきましょう。

# lists/tests.pyに追加

from lists.models import Item


class ItemModelTest(TestCase):

    def test_saving_and_retrieving_item(self):
        first_item = Item()
        first_item.text = 'The first (ever) list item'
        first_item.save()

        second_item = Item()
        second_item.text = 'Item the second'
        second_item.save()

        saved_items = Item.objects.all()
        self.assertEqual(saved_items.count(), 2)

        first_saved_item = saved_items[0]
        second_saved_item = saved_items[1]
        self.assertEqual(first_saved_item.text, 'The first (ever) list item')
        self.assertEqual(second_saved_item.text, 'Item the second')

モデルを定義する際に確認する点は以下の2点のようです。

  • モデルにデータが保存されているか

  • モデルからデータが取り出せているか

これを単体テストで確認します。

$ python manage.py test

======================================================================
ERROR: lists.tests (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: lists.tests
Traceback (most recent call last):
  File "C:\Users\you_name\AppData\Local\Programs\Python\Python37\lib\unittest\loader.py", line 436, in _find_test_path
    module = self._get_module_from_name(name)
  File "C:\Users\you_name\AppData\Local\Programs\Python\Python37\lib\unittest\loader.py", line 377, in _get_module_from_name
    __import__(name)
  File "C:--your_path--\django-TDD\lists\tests.py", line 4, in <module>
    from lists.models import Item
ImportError: cannot import name 'Item' from 'lists.models' (C:--your_path--\django-TDD\lists\models.py)

モデルを定義していないのでImportErrorとなりました。早速モデルを定義します。

# lists/models.py

from django.db import models


class Item(object):
    pass

単体テストを実行すると結果はAttributeError: 'Item' object has no attribute 'save'となります。
Itemクラスにsaveメソッドを追加するにはModel classを継承する必要があります。
lists/models.pyを書き換えましょう.

# lists/models.py

from django.db import models


class Item(models.Model):
    pass

単体テストの結果はdjango.db.utils.OperationalError: no such table: lists_itemとなります。
これはlists/models.pyItemクラスを追加したものの、実際のテーブルは作成されていないためです。

Our First Database Migration

DjangoのORMを使ってマイグレーションをしましょう。

$ python manage.py makemigrations
Migrations for 'lists':
  lists\migrations\0001_initial.py
    - Create model Item

listsフォルダ内に/migrationsというフォルダが作成されました。

単体テストを行うとAttributeError: 'Item' object has no attribute 'text'という結果になりました。

The Test Gets Surprisingly Far

Itemクラスにtextを追加していきます。

# lists/models.py

from django.db import models


class Item(models.Model):
    text = models.TextField()

単体テストを行うとdjango.db.utils.OperationalError: no such column: lists_item.textとなります。
これは作成したItemというテーブルにtextがまだ追加されていないためです。
追加するにはマイグレーションする必要があります。

$ python manage.py makemigrations

You are trying to add a non-nullable field 'text' to item without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py
Select an option: 2

models.TextField()にはdefault値を設定する必要があるようなので追加します。

# lists/models.py

from django.db import models


class Item(models.Model):
    text = models.TextField(default='')

モデルを変更したのでマイグレーションします。

$ python manage.py makemigrations
Migrations for 'lists':
  lists\migrations\0002_item_text.py
    - Add field text to item

これで単体テストはパスできました。
コミットしておきましょう。

$ git add lists
$ git commit -m "Model for list Items and associated migration"

Saving the POST to the Database

モデルの保存と取り出しが確認できたので、POSTリクエストの内容が保存できるかどうかを確認するテストを追加します。

# lists/tests.py

def test_can_save_a_POST_requset(self):
    data = {'item_text': 'A new list item'}
    response = self.client.post('/', data)

    # 追加されたかどうか
    self.assertEqual(Item.objects.count(), 1)

    # 正しく取り出せているかどうか
    new_item = Item.objects.first()
    self.assertEqual(new_item, data['item_text'])

    # 保存された内容がレスポンスされているか
    self.assertIn(data['item_text'], response.content.decode())

    # 指定したテンプレートを使用しているかどうか
    self.assertTemplateUsed(response, 'lists/home.html')

単体テストを実施してみましょう

$ python manage.py test

======================================================================
FAIL: test_can_save_a_POST_requset (lists.tests.ItemModelTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\--your_path--\django-TDD\lists\tests.py", line 43, in test_can_save_a_POST_requset
    self.assertEqual(Item.objects.count(), 1)
AssertionError: 0 != 1

AssertionError: 0 != 1はItemにデータが保存されていないために発生しているようです。
これを解決するためにViewを書き換えていきましょう。

# lists/views.py

from django.shortcuts import render
from lists.models import Item


def home_page(request):
    if request.method == 'POST':
        new_item_text = request.POST['item_text']
        Item.objects.create(text=new_item_text)  # 保存
    else:
        return render(request, 'lists/home.html')
    return render(request, 'lists/home.html', {
            'new_item_text': new_item_text,
        })

また、POSTだけでなくGETの場合のテストも追加しておく必要がありそうです。

class HomePageTest(TestCase):

    def test_users_home_template(self):
        response = self.client.get('/')  # URLの解決
        self.assertTemplateUsed(response, 'lists/home.html')

    def test_can_save_a_POST_requset(self):
        data = {'item_text': 'A new list item'}
        response = self.client.post('/', data)

        # 追加されたかどうか
        self.assertEqual(Item.objects.count(), 1)

        # 正しく取り出せているかどうか
        new_item = Item.objects.first()
        self.assertEqual(new_item, data['item_text'])

        # 保存された内容がレスポンスされているか
        self.assertIn(data['item_text'], response.content.decode())

        # 指定したテンプレートを使用しているかどうか
        self.assertTemplateUsed(response, 'lists/home.html')

    def test_only_saves_items_when_necessary(self):  # 追加
        self.client.get('/')
        self.assertEqual(Item.objects.count(), 0)

ViewをGETとPOSTの場合を考慮した書き方に変更しましょう。

# lists/views.py

from django.shortcuts import render
from lists.models import Item


def home_page(request):
    if request.method == 'POST':
        new_item_text = request.POST['item_text']
        Item.objects.create(text=new_item_text)
    else:
        new_item_text = ''
    return render(request, 'lists/home.html', {
            'new_item_text': new_item_text,
        })

これで単体テストを実行するとパスすることができました。

Redirect After Post

GETリクエストに対してnew_item_text = ''を返すのはあまり良い選択とは思えません。
Viewの役割は「ユーザーからのインプットを処理すること」と、「正しいレスポンスを返すこと」に分けられます。
今回は「正しいレスポンスを返すこと」について考えてみます。

POSTを受け取った後はredirectをさせるのが良い選択です(Always redirect agter a POST)。

単体テストをそのように書き換えましょう。

# lists/test.py

[...]

def test_can_save_a_POST_requset(self):
    self.client.post('/', data={'item_text': 'A new list item'})
    # 追加されたかどうか
    self.assertEqual(Item.objects.count(), 1)
    # 正しく取り出せているかどうか
    new_item = Item.objects.first()
    self.assertEqual(new_item.text, "A new list item")

def test_redirects_after_POST(self):
    response = self.client.post('/', data={'item_text': 'A new list item'})
    # POSTの後にリダイレクトされてい るか
    self.assertEqual(response.status_code, 302)
    # リダイレクト先が正しいかどうか
    self.assertEqual(response['location'], '/')

[...]

HTTP.redirectのステータスコードは302です。単体テストを実行します。
AssertionError: 200 != 302となりました。
POST後に('/')へリダイレクトするようにViewを書き換えます。

# lists/views.py

from django.shortcuts import render, redirect  # 追加
from lists.models import Item


def home_page(request):
    if request.method == 'POST':
        new_item_text = request.POST['item_text']
        Item.objects.create(text=new_item_text)
        return redirect('/')
    return render(request, 'lists/home.html')

POSTのリダイレクトが有効になるようにViewを変更しまた。これで単体テストはパスしました。

Rendering Items in the Templates

登録されたアイテムがリスト形式になって表示される機能をつくっていきましょう。
GETリクエストが来た時に全てのアイテムがHTMLに含まれていてほしいということになります。

# lists/tests.py

class HomePageTest(TestCase):
    [...]

    def test_displays_all_lits_items(self):
        Item.objets.create(text="itemey 1")
        Item.objets.create(text="itemey 2")

        response = self.client.get('/')

        self.assertIn('itemey 1', response.cotents.decode())
        self.assertIn('itemey 2', response.cotents.decode())

    [...]

現在のGETリクエストは単にhome.htmlを返すだけとなっています。
アイテムの一覧を返せるようにGET時のViewを変更しましょう。

# lists/views.py

def home_page(request):
    if request.method == 'POST':
        new_item_text = request.POST['item_text']
        Item.objects.create(text=new_item_text)
        return redirect('/')

    items = Item.objects.all()
    return render(request, 'lists/home.html', {'items': items})

単体テストを実施しましょう。

$ python manage.py test

======================================================================
FAIL: test_displays_all_lits_items (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\--your_path--\django-TDD\lists\tests.py", line 38, in test_displays_all_lits_items
    self.assertIn('itemey 1', response.content.decode())
AssertionError: 'itemey 1' not found in '<!-- lists/home.html -->\n<html>\n    <head>\n        <title>To-Do lists</title>\n    </head>\n
 <body>\n        <h1>Your To-Do list</h1>\n            <form method="post">\n                <input name="item_text" id="id_new_item" placeholder="Enter a to-do item">\n                <input type="hidden" name="csrfmiddlewaretoken" value="DX7a2J4eXPA2wxUPvoN6dKJbDKBZAzZ3XdLGQjQyTNkh6o7GE9jbOkWNAsR8kkVn">\n            </form>\n        <table id="id_list_table">\n            <tr><td>1: </td></tr>\n        </table>\n    </body>\n</html>\n'

----------------------------------------------------------------------

response.content内にitemey 1が表示されていないようです。templates/lists/home.htmlにテーブル一覧を表示させるように変更しましょう。

<!-- lists/home.html -->
[...]
<table id="id_list_table">
    {% for item in items %}
        <tr><td>{{ item.text }}</td></tr>
    {% endfor %}
</table>
[...]

これで単体テストはパスすることができました。単体テストが通ったので機能テストを実施しましょう。

$ python functional_tests.py

======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "functional_tests.py", line 28, in test_can_start_a_list_and_retrieve_it_later
    self.assertIn('To-Do', self.browser.title)
AssertionError: 'To-Do' not found in 'OperationalError at /'

----------------------------------------------------------------------

機能テストを確認すると最初のTo-Do表示すらできていないことがわかりました。
これは何か根本的な見落としがありそうです。開発サーバー(http://localhost:8000)にアクセスして画面を確認してみましょう。

するとOperationalError at / no such table: lists_itemと表示されています。
どうやら作成したと思っていたデータベースが出来ていないようです。

Creating Our Production Database with migrate

データベースが出来ていないのにも関わらず単体テストではエラーが発生しなかったのはなぜでしょうか?
DjangoのTestCaseを使った単体テストではテスト用のデータベースが単体テストを実行する度に作成されは破棄されているため、単体テストでは発生しなかったのです(すごい)。

したがって機能テストを実行するためにはデータベースを作成する必要があります。

$ python manage.py migrate

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, lists, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying lists.0001_initial... OK
  Applying lists.0002_item_text... OK
  Applying sessions.0001_initial... OK

python manage.py makemigrationsでデータベースの変更内容を記録していたので、python manage.py migrateで実際にデータベースを作成できたようです。
それでは機能テストを実施したいと思います。

$ python functional_tests.py

======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "functional_tests.py", line 46, in test_can_start_a_list_and_retrieve_it_later
    self.check_for_row_in_list_table('1: Buy dorayaki')
  File "functional_tests.py", line 21, in check_for_row_in_list_table
    self.assertIn(row_text, [row.text for row in rows])
AssertionError: '1: Buy dorayaki' not found in ['Buy dorayaki']

どうやらアイテムの表示が1: ~~のように数え上げられていないようです。
これはDjangoのテンプレートタグを使って解決できます。
lists/home.htmlのテーブル表示内容を変更しましょう。

<!-- lists/home.html -->
[...]
<table id="id_list_table">
    {% for item in items %}
        <tr><td>{{forloop.counter}}: {{ item.text }}</td></tr>
    {% endfor %}
</table>
[...]

機能テストを実施します。

$ python functional_tests.py


======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "functional_tests.py", line 56, in test_can_start_a_list_and_retrieve_it_later
    self.check_for_row_in_list_table('2: Demand payment for the dorayaki')
  File "functional_tests.py", line 21, in check_for_row_in_list_table
    self.assertIn(row_text, [row.text for row in rows])
AssertionError: '2: Demand payment for the dorayaki' not found in ['1: Buy dorayaki', '2: Buy dorayaki', '3: Demand payment for the dorayaki']

----------------------------------------------------------------------

Seleniumによって実行されたテスト画面を確認してみると、既に機能テストによって追加されてしまったBuy dorayakiがあることがわかります。これによって'2: Demand payment for the dorayaki'が正しく評価されていないようです。

したがって、一度db.sqplite3を削除して作り直したのち、機能テストを実施し直しましょう。

# db.sqlite3の削除
$ del db.sqplite3
# データベースを0から作り直す
$ python manage.py migrate --noinput

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, lists, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying lists.0001_initial... OK
  Applying lists.0002_item_text... OK
  Applying sessions.0001_initial... OK

機能テストを再度行うとAssertionError: Finish the test!となりました。
無事に機能テストはパスしたようです。

コミットしておきましょう。

$ git add .
$ git commit -m "Redirect after POST, and show all items in template"

Chapter5まとめ

今回はPOSTによるアイテムの受け取り、その保存をTDDで開発することができました。
POSTによるデータのやりとりにおいて今回テストした内容と、その根拠のをまとめておきます。

テスト内容 確認方法
POST POSTされたデータが保存されているかどうか POSTした後にデータの数が増えているかどうか
POSTされたデータを問題なく取り出せているかどうか 最新のデータとPOSTした内容が一緒かどうか
POSTした後正しいURLへリダイレクトできているかどうか POST後のresponseステータスが302かどうか、response['location']:が正しいredirect先か
GET 正しくテンプレートが返せているかどうか 指定テンプレートが使用されているかどうか
GETリクエストがPOSTリクエストと区別できているか GET時にアイテムの数が増えていないかどうか
GETリクエストの際にアイテム一覧が表示されているか リターンされている内容に追加したアイテムが含まれているかどうか
データ保存 正しくデータが保存されているかどうか アイテムを追加してアイテム数が追加した分だけ増えているかどうか
取り出したアイテムが保存させたデータと一致しているかどうか
1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3