もた日記

くだらないことを真面目にやる

Djangoメモ(12) : get_object_or_404ショートカットとリンクのテスト

Python 3.6.4 Django 2.0.2

A Complete Beginner's Guide to Djangoのチュートリアルを参考にget_object_or_404ショートカットの利用とリンクのテストをしてみる。


get_object_or_404ショートカット

前回作成したTopic一覧を表示する詳細ページのテストをするためにmyproject/boards/tests.pyBoardTopicsTestsを追加。

from django.urls import reverse, resolve
from django.test import TestCase
from .views import home, board_topics
from .models import Board

class HomeTests(TestCase):
    # 省略

class BoardTopicsTests(TestCase):
    def setUp(self):
        Board.objects.create(name='Django', description='Django board.')

    def test_board_topics_view_success_status_code(self):
        url = reverse('board_topics', kwargs={'pk': 1})
        response = self.client.get(url)
        self.assertEquals(response.status_code, 200)

    def test_board_topics_view_not_found_status_code(self):
        url = reverse('board_topics', kwargs={'pk': 99})
        response = self.client.get(url)
        self.assertEquals(response.status_code, 404)

    def test_board_topics_url_resolves_board_topics_view(self):
        view = resolve('/boards/1/')
        self.assertEquals(view.func, board_topics)

今回はsetUpメソッドを追加してテストで使用するインスタンスを生成するようにしている。
Djangoのテストは現在のデータベースに対して行うわけではなく、新しいデータベースをその都度生成(マイグレーションおよび破棄)して行っているためテスト用データの準備などはこのsetUpメソッド内で実施するのが良い。
テストの内容はステータスコードを確認するテストと正しいビュー関数が呼ばれているかの確認だが、テストを実行してみるとtest_board_topics_view_not_found_status_codeDoesNotExist例外で失敗する。

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.E...
======================================================================
ERROR: test_board_topics_view_not_found_status_code (boards.tests.BoardTopicsTests)
----------------------------------------------------------------------

 省略

boards.models.DoesNotExist: Board matching query does not exist.

----------------------------------------------------------------------
Ran 5 tests in 0.100s

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

実際にページにアクセスしてみると同じメッセージが表示される。

f:id:wonder-wall:20180311225056p:plain

DEBUG = Falseにしてみると500 Internal Server Errorになっていることがわかる。

f:id:wonder-wall:20180311225256p:plain

今回は404エラーが返ってくるようにしたいのでboards/views.pyboard_topicsを次のように編集する。Http404のインポートを忘れないように注意。

from django.shortcuts import render
from django.http import Http404
from .models import Board

def home(request):
    # 省略

def board_topics(request, pk):
    try:
        board = Board.objects.get(pk=pk)
    except Board.DoesNotExist:
        raise Http404
    return render(request, 'topics.html', {'board': board})

テストを実行してみると全部パスする。

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.037s

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

ページにアクセスしてみると404ページが表示されることが確認できる。 これはDjangoのデフォルトの404ページだが変更する事もできるようだ。

f:id:wonder-wall:20180311225830p:plain

DEBUG = Falseの場合の表示は下記。

f:id:wonder-wall:20180311225921p:plain

このget() を実行し、オブジェクトが存在しない場合にHttp404を送出することはよく使われるイディオムなので、 Djangoではget_object_or_404()ショートカットが用意されている。
ということでショートカットで書き換えてみる。import文も変更したので注意。

from django.shortcuts import render, get_object_or_404
from .models import Board

def home(request):
    # 省略

def board_topics(request, pk):
    board = get_object_or_404(Board, pk=pk)
    return render(request, 'topics.html', {'board': board})

このように1行で簡潔に書くことができる。
一応、python manage.py testを実行して結果が変わっていないことを確認しておく。


ナビゲーションリンクの追加とテスト

メインページから詳細ページへのリンクと詳細ページからメインページへのリンクを追加する。 これについては最初にテストを追加してから機能を実装してみる。


メインページの変更

HomeTestsを以下のように変更する。

class HomeTests(TestCase):
    def setUp(self):
        self.board = Board.objects.create(name='Django', description='Django board.')
        url = reverse('home')
        self.response = self.client.get(url)

    def test_home_view_status_code(self):
        self.assertEquals(self.response.status_code, 200)

    def test_home_url_resolves_home_view(self):
        view = resolve('/')
        self.assertEquals(view.func, home)

    def test_home_view_contains_link_to_topics_page(self):
        board_topics_url = reverse('board_topics', kwargs={'pk': self.board.pk})
        self.assertContains(self.response, 'href="{0}"'.format(board_topics_url))

setUpメソッドでBoardインスタンスを生成し、homeのレスポンスを再利用できるようにself.responseに設定する。
新しく追加したtest_home_view_contains_link_to_topics_pageテストではassertContainsメソッドを使用してhref="/boards/1/"が含まれているかを確認する。 テストを実行してみると機能はまだ実装していないので当然失敗する。

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....F.
======================================================================
FAIL: test_home_view_contains_link_to_topics_page (boards.tests.HomeTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vagrant/django/myproject/myproject/boards/tests.py", line 21, in test_home_view_contains_link_to_topics_page
    self.assertContains(self.response, 'href="{0}"'.format(board_topics_url))
  File "/home/vagrant/.local/share/virtualenvs/myproject-j-SR1M6H/lib/python3.6/site-packages/django/test/testcases.py", line 369, in assertContains
    self.assertTrue(real_count != 0, msg_prefix + "Couldn't find %s in response" % text_repr)
AssertionError: False is not true : Couldn't find 'href="/boards/1/"' in response

----------------------------------------------------------------------
Ran 6 tests in 0.040s

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

テストがパスするようにtemplates/home.htmlを編集(<tbody>内のみ表示)。

<tbody>
  {% for board in boards %}
    <tr>
      <td>
        <a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a>
        <small class="text-muted d-block">{{ board.description }}</small>
      </td>
      <td class="align-middle">0</td>
      <td class="align-middle">0</td>
      <td></td>
    </tr>
  {% endfor %}
</tbody>

テンプレートでURLを記述する場合は、URLの変更に対応できる{% url %}タグを使用するのが良い。 {% url %}タグの最初の引数(board_topics)はURLconfのnameで、残りは<int:pk>などに対する引数を指定する。
リンクを追加したのでこれでテストが通るようになる。

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......
----------------------------------------------------------------------
Ran 6 tests in 0.055s

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

メインページにアクセスしてみるとリンクが追加されている。

f:id:wonder-wall:20180311233853p:plain


詳細ページの変更

次に詳細ページからメインメージへのリンク用のテストを追加する。

class BoardTopicsTests(TestCase):
    # 省略

    def test_board_topics_view_contains_link_back_to_homepage(self):
        board_topics_url = reverse('board_topics', kwargs={'pk': 1})
        response = self.client.get(board_topics_url)
        homepage_url = reverse('home')
        self.assertContains(response, 'href="{0}"'.format(homepage_url))

実装していないので同じくテストは失敗する。

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.F.....
======================================================================
FAIL: test_board_topics_view_contains_link_back_to_homepage (boards.tests.BoardTopicsTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vagrant/django/myproject/myproject/boards/tests.py", line 45, in test_board_topics_view_contains_link_back_to_homepage
    self.assertContains(response, 'href="{0}"'.format(homepage_url))
  File "/home/vagrant/.local/share/virtualenvs/myproject-j-SR1M6H/lib/python3.6/site-packages/django/test/testcases.py", line 369, in assertContains
    self.assertTrue(real_count != 0, msg_prefix + "Couldn't find %s in response" % text_repr)
AssertionError: False is not true : Couldn't find 'href="/"' in response

----------------------------------------------------------------------
Ran 7 tests in 0.059s

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

templates/topics.htmlを編集してリンクを追加。

{% load static %}<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>{{ board.name }}</title>
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
  </head>
  <body>
    <div class="container">
      <ol class="breadcrumb my-4">
        <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
        <li class="breadcrumb-item active">{{ board.name }}</li>
      </ol>
    </div>
  </body>
</html>

これでテストが通るようになる。

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 0.047s

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

詳細ページにもリンクが付いたのでメインページとの行き来ができるようになった。

f:id:wonder-wall:20180311234639p:plain


まとめ

  • テストデータの準備などはsetUpメソッドで実施
  • get_object_or_404()ショートカットが提供されている
  • assertContainsはレスポンスのコンテンツ内に指定文字列が入っているかどうか確認
  • テンプレートでのURLの記述には{% url %}を使用