Test-Driven Development with Django - 1

從 Django Project 開始,我們需要先建立一個 Django 專案,而前置安裝在此簡單帶過,我們會使用到 virtualenv,而關於 venv 的安裝則不在此說明。

此篇 blog 文章為閱讀 ORelly Test-Driven Development with Python 一書之學習心得與紀錄,文中範例大多來自於此書。

Unit Test 和 Functional Test 的差別

Functional Test 是從程式外部來測試程式,也就是以使用者的觀點來做操作測試。而 Unit Test 則是從程式內部來測試,以工程師的觀點來做單元測試。

那麼我們接下來的步驟大概會像是這樣:

  1. 從使用者觀點來撰寫 Functional Test
  2. 畢竟是 TDD,所以寫完測試後要思考怎麼讓測試通過,而此時我們用 Unit Tests 來定義我們希望程式的行為為何,每一行程式碼都應該要被測試到(也就是所謂的 coverage rate)
  3. 寫完 Unit Test 就可以開始寫一些 code 了,但是寫這些 code 暫且是為了能夠通過測試。
  4. 接下來就能夠繼續測試 Functional Test 能不能過了。通常這個流程會一直重複:寫測試、寫 code、跑測試。

About Django

啟動 virtualenv 並開始著手安裝 Django

1
2
3
virtualenv venv --python=python3
. venv/bin/activate
pip install django # 你可以自己決定版本

開始一個新專案並且跑起來看看

1
2
3
python django-admin.py startproject myproject # 隨意幫自己的專案取個名字
cd myproject
python manage.py runserver

Django works result snapshot

開一個新的 APP 吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
$ python manage.py startapp lists
$ tree lists
lists
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│ └── __init__.py
├── models.py
├── tests.py
└── views.py

1 directory, 7 files

Django 的 MVC 架構

MVC 的全名是 Model-View-Controller(MVC) 的架構,在這個架構當中是這樣子:

  • Model: 實作演算法、資料庫存取等功能
  • View: 界面設計,把資料呈現給使用者的角色
  • Controller: 針對請求去作處理

但是在 Django 裡面卻有著些微的差異,Django 有下列幾個角色:

  • View: 某個網址會對應到的 Callback Function,相當於 Model 的角色
  • Template: 對應到原本 MVC 架構中的 View
  • 那 Controller 跑到哪去了呢?對應之下,就是 Django 本身的整個應用程式了,因為 Django 會去負責處理請求,並把他根據 url.py 對應到正確的 View 當中

註:Callback Function: 當某網址被存取時,Django 會去觸發的 Function,這個會在 url.py 被定義

About Django Test

在一開始,我們從最簡單的做起,我們現在要測試的就:

  • 我們 Django Project 的根目錄是否能對應到正確的 view function

First Test

我們的 lists/tests.py 內容如下:

1
2
3
4
5
6
7
8
9
from django.core.urlresolvers import resolve
from django.test import TestCase
from lists.views import home_page

class HomePageTest(TestCase):

def test_root_url_resolves_to_home_page_view(self):
found = resolve('/')
self.assertEqual(fount.func, home_page)

寫完 Test 之後能夠執行看看,python manage.py test 指令一下之後會看到以下訊息:

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
$ python manage.py test
Creating test database for alias 'default'...
E
======================================================================
ERROR: lists.tests (unittest.loader.ModuleImportFailure)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/lib/python3.4/unittest/case.py", line 58, in testPartExecutor
yield
File "/usr/lib/python3.4/unittest/case.py", line 577, in run
testMethod()
File "/usr/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 "/usr/lib/python3.4/unittest/loader.py", line 312, in _find_tests
module = self._get_module_from_name(name)
File "/usr/lib/python3.4/unittest/loader.py", line 290, in _get_module_from_name
__import__(name)
File "/home/aweimeow/blog-django/myproject/lists/tests.py", line 3, in <module>
from lists.views import home_page
ImportError: cannot import name 'home_page'


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

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

而到了這個地步就能看到我們的第一個 Test 做完了,從訊息當中可以觀察到我們還沒寫 home_page 這個 Callback Function,所以要在 lists/views.py 撰寫我們的 home_page

但是因為我們這邊看到的錯誤訊息是:

1
ImportError: cannot import name 'home_page'

所以我們就先在 lists/views.py 寫下以下內容:

1
2
3
from django.shortcuts import render

home_page = None

雖然這樣看起來很奇怪,但是這樣子就不會出現 ImportError 了,所以讓我們再測試一次。

Read Exception log

再執行之後錯誤訊息則如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Creating test database for alias 'default'...
E
======================================================================
ERROR: test_root_url_resolves_to_home_page_view (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/aweimeow/blog-django/myproject/lists/tests.py", line 8, in test_root_url_resolves_to_home_page_view
found = resolve('/')
File "/home/aweimeow/blog-django/venv/lib/python3.4/site-packages/django/urls/base.py", line 27, in resolve
return get_resolver(urlconf).resolve(path)
File "/home/aweimeow/blog-django/venv/lib/python3.4/site-packages/django/urls/resolvers.py", line 300, in resolve
raise Resolver404({'tried': tried, 'path': new_path})
django.urls.exceptions.Resolver404: {'tried': [[<RegexURLResolver <RegexURLPattern list> (admin:admin) ^admin/>]], 'path': ''}

----------------------------------------------------------------------
Ran 1 test in 0.025s

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

這個錯誤訊息要分幾個部份來閱讀,我們閱讀的順序應該如下:

  1. django.urls.exceptions.Resolver404
    • 第一眼看見的是我們碰到的 Error 究竟是什麼呢
    • 這個例子當中,則是在 django resolve path 的時候找不到(404 Not Found)
  2. ERROR: test_root_url_resolves_to_home_page_view
    • 接下來會被看到的則是在我們寫的哪一個 Test 出了問題
  3. found = resolve(‘/‘)
    • 最後則會是我們發生錯誤的那一行程式碼

總而言之,會發生這個錯誤是因為在 Django 中定義網址應該被導向至哪個 Function 沒有被定義(所以找不到),因此我們需要了解怎麼撰寫定義網址導向規則的 url.py

MVC’s Controller in Django(url.py)

url.py 當中的規則都是利用 正則表示式 Regular Expression 撰寫,如果有需要學習 regexp 的話,我很推薦 RegexOne 這個網站。

回到正題,在 url.py 當中一開始只會有 admin 頁面有被定義,內容如下:

1
2
3
4
5
6
from django.conf.urls import url
from django.contrib import admin

urlpatterns = [
url(r'^admin/', admin.site.urls),
]

我們需要做的事情便是 import 我們的 home_page,因此我們寫下:

1
2
3
4
5
6
7
8
9
from django.conf.urls import url
from django.contrib import admin

from lists.views import home_page

urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$', home_page, name='home'),
]

Run Test

執行之後,你會發現出現這個錯誤訊息:

1
TypeError: view must be a callable or a list/tuple in the case of include().

這個訊息告訴我們,一個被放進 url.py 的 view,應該是 callable(可被呼叫的,也就是 Function)。

所以我們就來小小修改一下把他變成 Function 吧,修改一下 lists/views.py 來達成這個目標:

1
2
def home_page():
pass

接著再跑一次 Test:

1
2
3
4
5
6
7
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.002s

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

到這邊就暫時到一段落,我們這回做的事情只有寫關於能否對應到正確的 Function 的 Test,接下來將會針對網頁的內容做一些測試。