Django 1.6 的測試驅動開發
測試驅動開發(TDD)是一個迭代的開發周期,強調編寫實際代碼之前編寫自動化測試。
這個過程很簡單:
-
先編寫測試。
-
查看測試失敗的地方
-
編寫足夠的代碼以使測試通過。
-
再次測試。
-
代碼重構 。
-
重復以上操作。
目錄
這是一個長的帖子,為了您的方便所以提供了目錄:
-
什么是TDD
-
第一次測試
-
安裝Django
-
功能測試
-
管理員登錄
-
設置聯系人應用程序
5.單位測試
-
主界面
-
所有聯系人視圖
-
添加聯系人視圖
-
視圖
-
驗證
-
創建聯系人
6.功能測試完回家
7.結構測試
8,總結
為什么要用TDD?
使用TDD,你將學會把你的代碼拆分成符合邏輯的,簡單易懂的片段,這有助于確保代碼的正確性。
這一點非常重要,因為做到下面這些事情是非常困難的:
-
在我們的腦中一次性處理所有復雜的問題。
-
了解何時從哪里開始著手解決問題。
-
在代碼庫的復雜度不斷增長的同時不引入錯誤和bug;并且
-
辨別出代碼在什么時候發生了問題。
TDD幫助我們定位問題。它不能保證你的代碼完全沒有錯誤;然而,你可以寫出更好的代碼,從而能更好地理解理解代碼。這本身有助于消除錯誤,并且至少,你可以更容易的定位錯誤。
TTD實際上也是一種行業標準。
說的夠多了。讓我們來看看代碼吧。
在這個教程里,我們將創建一個存儲用戶聯系人的app。
請注意: 這篇教程假設你運行在一個基于Unix的環境里 - 例如, Mac OSX, Linux, 或者在Windows下的Linux VM。 我將使用Sublime 2作為文本編輯器。并且,確保你已經完成了官方的Django教程并且基本了解Python語言. 此外,在這個第一篇post里,我們不會涉及到Django1.6提供的新工具。這篇文章將為之后的post打好基礎來處理不同形式的測試。
第一個測試
在開始做一些事情之前,我們需要首先創建一個測試。為了這個測試,我們需要讓Django正確安裝。為此我們將使用一個函數測試——這在下面會詳細解釋。
-
創建一個新目錄存放你的項目:
$ mkdir django-tdd $ cd django-tdd
-
再建立一個目錄存放函數測試
$ mkdir ft $ cd ft
-
創建一個新文件 "tests.py"并加入以下代碼:
from selenium import webdriver browser = webdriver.Firefox()browser.get('http://localhost:8000/')body = browser.find_element_by_tag_name('body')assert 'Django' in body.text browser.quit()
-
現在運行測試:
$ python tests.py
確認安裝selenium(譯注:自動化測試軟件)時是使用 installed -pip安裝的
你將看到 FireFox彈出來試圖打開 http://localhost:8000/。在你的終端上面你會看到:
Traceback (most recent call last):File "tests.py", line 7, in <module>assert 'Django' in body.textAssertionError
祝賀!你完成了第一個失效測試。
現在我們寫足夠的代碼來讓它通過,這些代碼量約相當于設置一個 Django 開發環境。
設置Django
1. 激活一個virtualenv:
$ cd .. $ virtualenv --no-site-packages env $ source env/bin/activate
2. 安裝Django并且建立一個項目
$ pip install django==1.6.1$ django-admin.py startproject contacts
你當前的項目結構應該是下面這個樣子:
├── contacts │ ├── contacts │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ └── manage.py └── ft └── tests.py
3. 安裝 Selenium:
pip install selenium==2.39.0
4. 運行server
$ cd contacts $ python manage.py runserver
5. 接著,打開一個新終端窗口,定位到"ft"文件夾下,再運行一次測試:
$ python tests.py
你將看到FireFox又一次窗口導航到了http://localhost:8000/。這次應該沒有錯誤了。你剛剛已經通過了你的第一個測試!現在,讓我們完成環境設置。
6. 版本控制,首先添加一個".gitignore"并且在里面添加下面的代碼:
.Pythonenv bin lib include.DS_Store.pyc
現在來創建一個Git倉庫然后提交吧
$ git init $ git add .$ git commit -am "initial"
7. 項目建完了,現在我們回頭討論一下功能測試吧。
功能測試
我們通過用 Selenium 來進行第一次測試。這樣的測試會使我們使用web瀏覽器就像我們是最終用戶一樣,來看看應用程序實際上是怎么運行的。因為這些測試是遵循最終用戶的行為習慣——也可以說是用戶用例——這個包含了對一系列產品特點進行測試,而不僅僅對單一功能進行測試——這種更適合單元測試。有一點非常需要注意的是,當這部分測試代碼你還沒開始寫,那么你必須先從功能測試開始。由于我們基本上是測試Django的代碼,所以功能測試是一個正確的方法去做的。
另一種方式去思考功能測試和單元測試的區別,就是功能測試主要關注在應用程序的外部,從用戶的角度來進行測試,而單元測試主要是關注在應用程序的內部,從開發的角度進行測試。
在實踐中會更多地體現這個概念。
在繼續下個話題之前,我們先來重構我們的測試環境,使得測試起來更加簡單。
-
首先,我們要重寫在“tests.py”文件內的第一個測試:
from selenium import webdriverfrom selenium.webdriver.common.keys import Keysfrom django.test import LiveServerTestCaseclass AdminTest(LiveServerTestCase): def setUp(self): self.browser = webdriver.Firefox() def tearDown(self): self.browser.quit() def test_admin_site(self): # user opens web browser, navigates to admin page self.browser.get(self.live_server_url + '/admin/') body = self.browser.find_element_by_tag_name('body') self.assertIn('Django administration', body.text)
-
然后運行它:
$ python manage.py test ft
它會通過:
----------------------------------------------------------------------Ran 1 test in 3.304sOK
恭喜你!
在繼續之前,我們先看看這里是怎么回事。如果所有都通過了,你也會看到FireFox瀏覽器被打開,然后按照我們在測試里所用的setUp()和tearDown()方法設置的功能進行整個過程。這個測試本身只是簡單的測試這個"/admin" (self.browser.get(self.live_server_url + '/admin/')頁面是否被找到,"Django administration"這個單詞是否出現在body標簽內。
讓我們確認一下。
-
運行服務:
$ python manage.py runserver
在地址欄里敲上地址 http://localhost:8000/admin/ 你會看到:
-
我們可以只需對錯誤的東西進行簡單地測試便能確認測試是否正確運作。更新測試里的最后一行:
self.assertIn('administration Django', body.text)
重新再運行一次。你會發現有以下的錯誤(當然是我們所期望的):
AssertionError: 'administration Django' not found in u'Django administration\nUsername:\nPassword:\n '
修正測試,再測試一遍,就可以提交代碼了。
最后,你有沒有注意到,我們用來進行實際測試的功能名稱均以test_開頭。這是為了讓Django測試運行器能找到這些測試。換句話來說,任何一個以test_開頭命名的功能都會被測試運行器視為一個測試。
管理員登陸
接下來,讓我們來測試,以確保用戶可以登錄到管理網站。
-
更新“tests.py”文件中的test_admin_site功能:
def test_admin_site(self): # user opens web browser, navigates to admin page self.browser.get(self.live_server_url + '/admin/') body = self.browser.find_element_by_tag_name('body') self.assertIn('Django administration', body.text) # users types in username and passwords and presses enter username_field = self.browser.find_element_by_name('username') username_field.send_keys('admin') password_field = self.browser.find_element_by_name('password') password_field.send_keys('admin') password_field.send_keys(Keys.RETURN) # login credentials are correct, and the user is redirected to the main admin page body = self.browser.find_element_by_tag_name('body') self.assertIn('Site administration', body.text)
所以 -
-
find_element_by_name- 是用于定位輸入框。
-
send_keys- 發送鍵盤按鍵信息。
-
運行測試,你會發現這個錯誤:
AssertionError: 'Site administration' not found in u'Django administration\nPlease enter the correct username and password for a staff account. Note that both fields may be case-sensitive.\nUsername:\nPassword:\n '
這個之所以會失敗,是因為我們沒有管理員用戶設置。這是一個預期中的失敗,所以出現這種情況是對的。換句話來說,我們知道它會失敗的,這使得我們更容易去解決它。
-
同步數據庫:
$ python manage.py syncdb
設置一個管理員用戶。
再重新測試一遍。它依舊會失敗。為什么呢?因為Django在運行的時候會給我們數據庫創建一份副本,這樣的測試方式不會影響生產數據庫。
-
我們需要設置一個Fixture,是一個包含了我們想加載到測試數據庫的數據文件:登錄憑據。為了要實現這一點,當運行以下命令時,能夠將數據庫管理員用戶信息從數據庫轉存到Fixture中去:
$ mkdir ft/fixtures $ python manage.py dumpdata auth.User --indent=2 > ft/fixtures/admin.json
現在更新AdminTest類:
class AdminTest(LiveServerTestCase): # load fixtures fixtures = ['admin.json'] def setUp(self): self.browser = webdriver.Firefox() def tearDown(self): self.browser.quit() def test_admin_site(self): # user opens web browser, navigates to admin page self.browser.get(self.live_server_url + '/admin/') body = self.browser.find_element_by_tag_name('body') self.assertIn('Django administration', body.text) # users types in username and passwords and presses enter username_field = self.browser.find_element_by_name('username') username_field.send_keys('admin') password_field = self.browser.find_element_by_name('password') password_field.send_keys('admin') password_field.send_keys(Keys.RETURN) # login credentials are correct, and the user is redirected to the main admin page body = self.browser.find_element_by_tag_name('body') self.assertIn('Site administration', body.text)
運行這個測試,它會通過。
每次運行測試的時候,Django都會轉存測試數據庫。而這所有的Fixture都會在“test.py”文件中被指定加載到數據庫中去。
-
讓我們加一個或多個斷言。再次更新測試:
def test_admin_site(self): # user opens web browser, navigates to admin page self.browser.get(self.live_server_url + '/admin/') body = self.browser.find_element_by_tag_name('body') self.assertIn('Django administration', body.text) # users types in username and passwords and presses enter username_field = self.browser.find_element_by_name('username') username_field.send_keys('admin') password_field = self.browser.find_element_by_name('password') password_field.send_keys('admin') password_field.send_keys(Keys.RETURN) # login credentials are correct, and the user is redirected to the main admin page body = self.browser.find_element_by_tag_name('body') self.assertIn('Site administration', body.text) # user clicks on the Users link user_link = self.browser.find_elements_by_link_text('Users') user_link[0].click() # user verifies that user live@forever.com is present body = self.browser.find_element_by_tag_name('body') self.assertIn('live@forever.com', body.text)
-
運行它,它會失敗,因為我們需要添加另一個用戶到fixture文件中:
[{"pk": 1, "model": "auth.user", "fields": { "username": "admin", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": true, "last_login": "2013-12-29T03:49:13.545Z", "groups": [], "user_permissions": [], "password": "pbkdf2_sha256$12000$VtsgwjQ1BZ6u$zwnG+5E5cl8zOnghahArLHiMC6wGk06HXrlAijFFpSA=", "email": "ad@min.com", "date_joined": "2013-12-29T03:49:13.545Z"}},{"pk": 2, "model": "auth.user", "fields": { "username": "live", "first_name": "", "last_name": "", "is_active": true, "is_superuser": false, "is_staff": false, "last_login": "2013-12-29T03:49:13.545Z", "groups": [], "user_permissions": [], "password": "pbkdf2_sha256$12000$VtsgwjQ1BZ6u$zwnG+5E5cl8zOnghahArLHiMC6wGk06HXrlAijFFpSA=", "email": "live@forever.com", "date_joined": "2013-12-29T03:49:13.545Z"}}]
再次運行,它是會通過的。如果需要可以重構一下這個測試。現在想想還有什么可以測試。或許你可以測試管理員用戶可以添加一個用戶到管理面板中,或者可以測試沒有管理員權限的人是不能進入管理面板中。寫幾個測試,更新你的代碼,再次測試,根據需要重構代碼。
接下來,我們會添加增加聯系人應用,不要忘了提交代碼哦!
設置聯系人應用
-
開始一個測試,添加以下功能:
def test_create_contact_admin(self): self.browser.get(self.live_server_url + '/admin/') username_field = self.browser.find_element_by_name('username') username_field.send_keys('admin') password_field = self.browser.find_element_by_name('password') password_field.send_keys('admin') password_field.send_keys(Keys.RETURN) # user verifies that user_contacts is present body = self.browser.find_element_by_tag_name('body') self.assertIn('User_Contacts', body.text)
-
再次運行測試,你會看到以下錯誤:
AssertionError: 'User_Contacts' not found in u'Django administration\nWelcome, admin. Change password / Log out\nSite administration\nAuth\nGroups\nAdd\nChange\nUsers\nAdd\nChange\nRecent Actions\nMy Actions\nNone available'
這是預料之中的。
現在,我們要寫足夠的代碼讓它通過。
-
新建一個應用:
$ python manage.py startapp user_contacts
-
添加到“settings.py”文件:
INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'ft', 'user_contacts',)
-
在user_contacts目錄下的“admin.py”文件中添加以下代碼:
from user_contacts.models import Person, Phonefrom django.contrib import admin admin.site.register(Person)admin.site.register(Phone)
-
你的工程架構會跟如下類似:
.├── user_contacts │ ├── __init__.py │ ├── admin.py │ ├── models.py │ ├── tests.py │ └── views.py ├── contacts │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── ft │ ├── __init__.py │ ├── fixtures │ │ └── admin.json │ └── tests.py └── manage.py
-
更新“models.py”:
from django.db import modelsclass Person(models.Model): first_name = models.CharField(max_length = 30) last_name = models.CharField(max_length = 30) email = models.EmailField(null = True, blank = True) address = models.TextField(null = True, blank = True) city = models.CharField(max_length = 15, null = True,blank = True) state = models.CharField(max_length = 15, null = True, blank = True) country = models.CharField(max_length = 15, null = True, blank = True) def __unicode__(self): return self.last_name +", "+ self.first_nameclass Phone(models.Model): person = models.ForeignKey('Person') number = models.CharField(max_length=10) def __unicode__(self): return self.number
-
再次運行測試,你會看到:
Ran 2 tests in 11.730sOK
-
我們繼續下一步驟,添加測試進去以保證管理員可以添加數據:
# user clicks on the Persons link persons_links = self.browser.find_elements_by_link_text('Persons') persons_links[0].click() # user clicks on the Add person link add_person_link = self.browser.find_element_by_link_text('Add person') add_person_link.click() # user fills out the form self.browser.find_element_by_name('first_name').send_keys("Michael") self.browser.find_element_by_name('last_name').send_keys("Herman") self.browser.find_element_by_name('email').send_keys("michael@realpython.com") self.browser.find_element_by_name('address').send_keys("2227 Lexington Ave") self.browser.find_element_by_name('city').send_keys("San Francisco") self.browser.find_element_by_name('state').send_keys("CA") self.browser.find_element_by_name('country').send_keys("United States") # user clicks the save button self.browser.find_element_by_css_selector("input[value='Save']").click() # the Person has been added body = self.browser.find_element_by_tag_name('body') self.assertIn('Herman, Michael', body.text) # user returns to the main admin screen home_link = self.browser.find_element_by_link_text('Home') home_link.click() # user clicks on the Phones link persons_links = self.browser.find_elements_by_link_text('Phones') persons_links[0].click() # user clicks on the Add phone link add_person_link = self.browser.find_element_by_link_text('Add phone') add_person_link.click() # user finds the person in the drop downel = self.browser.find_element_by_name("person") for option in el.find_elements_by_tag_name('option'): if option.text == 'Herman, Michael': option.click() # user adds the phone numbers self.browser.find_element_by_name('number').send_keys("4158888888") # user clicks the save button self.browser.find_element_by_css_selector("input[value='Save']").click() # the Phone has been added body = self.browser.find_element_by_tag_name('body') self.assertIn('4158888888', body.text) # user logs out self.browser.find_element_by_link_text('Log out').click() body = self.browser.find_element_by_tag_name('body') self.assertIn('Thanks for spending some quality time with the Web site today.', body.text)
這就是管理員的功能。讓我們轉過頭來專注于user_contacts本身。你之前的代碼還記得提交嗎?如果沒有,趕緊提交吧!
單元測試
考慮下我們現在已經寫的特性。我們已經定義了我們的模型,允許管理員更改模型。根據這個情況和我們項目的整體目標,著重關注剩下的用戶功能。
用戶應該可以——
-
瀏覽所有的聯系人。
-
添加新的聯系人。
根據這些需求,嘗試把剩下的功能測試公式化。盡管,在我們寫功能測試之前,我們應該通過單元測試定義代碼的行為——這有助于你寫出良好、干凈的代碼,編寫功能測試更加簡單。
記住:功能測試最終將表示你的項目是否工作,而單元測試有助于你達到這樣的目的。這很快就會變的有意義。
讓我們暫停片刻,談論一些常規慣例。
盡管TDD(或者終端)的基礎——測試、代碼、重構——是通用的,很多開發者使用的方法是不同的。例如,我喜歡先寫單元測試,保證我們的代碼在細粒度級別有效,然后寫功能測試。其他開發者先寫功能測試,查看它們失敗,然后寫單元測試,查看它們失敗,然后再寫代碼,首先滿足單元測試,最終也應該滿足功能測試。這里沒有正確和錯誤的答案。哪種方法舒服用哪種——但繼續先測試、然后寫代碼,最后重構。
視圖
首先,檢查所有視圖都設置準確。
主視圖
-
跟往常一樣,先開始一個測試:
from django.template.loader import render_to_stringfrom django.test import TestCase, Clientfrom user_contacts.models import Person, Phonefrom user_contacts.views import *class ViewTest(TestCase):def setUp(self): self.client_stub = Client()def test_view_home_route(self): response = self.client_stub.get('/') self.assertEquals(response.status_code, 200)
-
給這個測試文件取名為test_views.py,并保存到user_contacts/tests目錄下。同時要添加__init__.py文件到目錄中去,在user_contacts主目錄下刪除"tests.py"文件。
-
運行它:
$ python manage.py test user_contacts
它會失敗的 -AssertionError: 404 != 200- 因為URL、視圖和模板都還沒存在。如果你不熟悉Django如何處理MVC架構,請點擊這里閱覽這篇簡短的文章。我們首先獲取用客戶端獲取url的“/”地址,這事Django的TestCase的一部分。這個響應被存儲起來,然后我們去檢查返回的狀態碼是否等于200。
-
添加如下路徑到“contacts/urls.py”:
url(r'^', include('user_contacts.urls')),
-
更新“contacts/urls.py”:
from django.conf.urls import patterns, urlfrom user_contacts.views import *urlpatterns = patterns('', url(r'^$', home),)
-
更新“views.py”:
from django.http import HttpResponse, HttpResponseRedirectfrom django.shortcuts import render_to_response, renderfrom django.template import RequestContextfrom user_contacts.models import Phone, Person# from user_contacts.new_contact_form import ContactFormdef home(request): return render_to_response('index.html')
-
添加“index.html”模板到模板目錄中去:
<!DOCTYPE html><html> <head> <title>Welcome.</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link > <style> .container { padding: 50px; } </style> </head> <body> <div class="container"> <h1>What would you like to do?</h1> <ul> <li><a href="/all">View Contacts</a></li> <li><a href="/add">Add Contact</a></li> </ul> <div> <script src="http://code.jquery.com/jquery-1.10.2.min.js"></script> <script src="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> </body></html>
-
再次運行測試,它就會順利通過。
所有聯系人視圖
對這個視圖的測試幾乎和我們上一個測試相同。在看我的答案之前先自己試試吧。
1.通過在ViewTest類里添加下面的方法來開始這個測試。
def test_view_contacts_route(self): response = self.client_stub.get('/all/') self.assertEquals(response.status_code, 200)
2. 在運行時,你將看到同樣的錯誤:AssertionError: 404 != 200 。
3. 用下面的路由策略更新"user_contacts/urls.py":
url(r'^all/$', all_contacts),
4. 更新"view.py":
def all_contacts(request): contacts = Phone.objects.all() return render_to_response('all.html', {'contacts':contacts})
5. 在templates文件夾里加入一個叫"all.html"的模板:
<!DOCTYPE html><html><head><title>All Contacts.</title><meta name="viewport" content="width=device-width, initial-scale=1.0"><link ><style> .container { padding: 50px; }</style></head><body><div class="container"> <h1>All Contacts</h1> <table border="1" cellpadding="5"> <tr> <th>First Name</th> <th>Last Name</th> <th>Address</th> <th>City</th> <th>State</th> <th>Country</th> <th>Phone Number</th> <th>Email</th> </tr> {% for contact in contacts %} <tr> <td></td> <td></td> <td></td> <td></td> <td></td> <td></td> <td></td> <td></td> </tr> {% endfor %} </table> <br> <a href="/">Return Home</a></div><script src="http://code.jquery.com/jquery-1.10.2.min.js"></script><script src="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script></body></html>
6. 然后測試應該能通過了。
增加聯系人視圖
這個測試與前面兩個稍有不同,所以一定要仔細的跟著下列步驟走。
1. 在test suite里加入測試:
def test_add_contact_route(self): response = self.client_stub.get('/add/') self.assertEqual(response.status_code, 200)
2. 你將在運行時看到這樣的錯誤:AssertionError: 404 != 200
3. 更新"urls.py":
url(r'^add/$', add),
4. 更新"views.py"
def add(request):person_form = ContactForm()return render(request, 'add.html', {'person_form' : person_form}, context_instance = RequestContext(request))
確保加入了如下的引用:
from user_contacts.new_contact_form import ContactForm
5. 創建一個叫 new_contact_form.py的新文件然后加入如下代碼:
import refrom django import formsfrom django.core.exceptions import ValidationErrorfrom user_contacts.models import Person, Phoneclass ContactForm(forms.Form): first_name = forms.CharField(max_length=30) last_name = forms.CharField(max_length=30) email = forms.EmailField(required=False) address = forms.CharField(widget=forms.Textarea, required=False) city = forms.CharField(required=False) state = forms.CharField(required=False) country = forms.CharField(required=False) number = forms.CharField(max_length=10) def save(self): if self.is_valid(): data = self.cleaned_data person = Person.objects.create(first_name=data.get('first_name'), last_name=data.get('last_name'), email=data.get('email'), address=data.get('address'), city=data.get('city'), state=data.get('state'), country=data.get('country')) phone = Phone.objects.create(person=person, number=data.get('number')) return phone
6. 加入"add.html"到模板文件夾里:
import refrom django import formsfrom django.core.exceptions import ValidationErrorfrom user_contacts.models import Person, Phoneclass ContactForm(forms.Form): first_name = forms.CharField(max_length=30) last_name = forms.CharField(max_length=30) email = forms.EmailField(required=False) address = forms.CharField(widget=forms.Textarea, required=False) city = forms.CharField(required=False) state = forms.CharField(required=False) country = forms.CharField(required=False) number = forms.CharField(max_length=10) def save(self): if self.is_valid(): data = self.cleaned_data person = Person.objects.create(first_name=data.get('first_name'), last_name=data.get('last_name'), email=data.get('email'), address=data.get('address'), city=data.get('city'), state=data.get('state'), country=data.get('country')) phone = Phone.objects.create(person=person, number=data.get('number')) return phone
7. 是不是通過了?應該是的。如果沒有,再檢查一下。
驗證
現在我們已經完成了視圖的測試,讓我們添加對表單的驗證。但首先我們要寫一個測試,驚喜吧!
-
在“tests”目錄下新增一個叫“test_validator.py”的文件并增加以下代碼:
from django.core.exceptions import ValidationError from django.test import TestCase from user_contacts.validators import validate_number, validate_string class ValidatorTest(TestCase): def test_string_is_invalid_if_contains_numbers_or_special_characters(self): with self.assertRaises(ValidationError): validate_string('@test') validate_string('tester#') def test_number_is_invalid_if_contains_any_character_except_digits(self): with self.assertRaises(ValidationError): validate_number('123ABC') validate_number('75431#')
-
在運行測試之前,你猜猜會有什么情況發生?提示:請密切注意代碼上面導入進來的包。你會有以下錯誤信息,因為我們沒有“validators.py”文件:
ImportError: cannot import name validate_string
換言之,我們測試所需的邏輯驗證文件還不存在。
-
在“user_contacts”目錄下新增一個叫“validators.py”的文件:
import refrom django.core.exceptions import ValidationErrordef validate_string(string): if re.search('^[A-Za-z]+$', string) is None: raise ValidationError('Invalid')def validate_number(value): if re.search('^[0-9]+$', value) is None: raise ValidationError('Invalid')
-
再次運行測試。5個測試會通過的:
Ran 5 tests in 0.019sOK
新增聯系人
-
由于我們增加了驗證,我們想測試一下在管理員區域這個驗證功能是可以工作的,所以更新“test_views.py”:
from django.template.loader import render_to_stringfrom django.test import TestCase, Clientfrom user_contacts.models import Person, Phonefrom user_contacts.views import *class ViewTest(TestCase): def setUp(self): self.client_stub = Client() self.person = Person(first_name = 'TestFirst',last_name = 'TestLast') self.person.save() self.phone = Phone(person = self.person,number = '7778889999') self.phone.save() def test_view_home_route(self): response = self.client_stub.get('/') self.assertEquals(response.status_code, 200) def test_view_contacts_route(self): response = self.client_stub.get('/all/') self.assertEquals(response.status_code, 200) def test_add_contact_route(self): response = self.client_stub.get('/add/') self.assertEqual(response.status_code, 200) def test_create_contact_successful_route(self): response = self.client_stub.post('/create',data = {'first_name' : 'testFirst', 'last_name':'tester', 'email':'test@tester.com', 'address':'1234 nowhere', 'city':'far away', 'state':'CO', 'country':'USA', 'number':'987654321'}) self.assertEqual(response.status_code, 302) def test_create_contact_unsuccessful_route(self): response = self.client_stub.post('/create',data = {'first_name' : 'tester_first_n@me', 'last_name':'test', 'email':'tester@test.com', 'address':'5678 everywhere', 'city':'far from here', 'state':'CA', 'country':'USA', 'number':'987654321'}) self.assertEqual(response.status_code, 200) def tearDown(self): self.phone.delete() self.person.delete()
兩個測試會失敗。
我們要怎么做才能讓測試通過呢?首先我們要為添加數據到數據庫增加一個視圖功能來查看。
-
添加路徑:
url(r'^create$', create),
-
更新“views.py”:
def create(request): form = ContactForm(request.POST)if form.is_valid(): form.save() return HttpResponseRedirect('all/')return render(request, 'add.html', {'person_form' : form}, context_instance = RequestContext(request))
-
再次測試:
$ python manage.py test user_contacts
這次只有一個測試會失敗 - AssertionError: 302 != 200 - 因為我們嘗試添加一些不通過驗證的數據但添加成功了。換言之,我們需要更新“models.py”文件中的表單都要把驗證考慮進去。
-
更新“models.py”:
from django.db import modelsfrom user_contacts.validators import validate_string, validate_numberclass Person(models.Model): first_name = models.CharField(max_length = 30, validators = [validate_string]) last_name = models.CharField(max_length = 30, validators = [validate_string]) email = models.EmailField(null = True, blank = True) address = models.TextField(null = True, blank = True) city = models.CharField(max_length = 15, null = True,blank = True) state = models.CharField(max_length = 15, null = True, blank = True, validators = [validate_string]) country = models.CharField(max_length = 15, null = True, blank = True) def __unicode__(self): return self.last_name +", "+ self.first_nameclass Phone(models.Model): person = models.ForeignKey('Person') number = models.CharField(max_length=10, validators = [validate_number]) def __unicode__(self): return self.number
-
刪除當前的數據庫,“db.sqlite3”,重新同步數據庫:
$ python manage.py syncdb
再次設置一個管理員賬戶。
-
新增驗證,更新new_contact_form.py:
import refrom django import formsfrom django.core.exceptions import ValidationErrorfrom user_contacts.models import Person, Phonefrom user_contacts.validators import validate_string, validate_numberclass ContactForm(forms.Form): first_name = forms.CharField(max_length=30, validators = [validate_string]) last_name = forms.CharField(max_length=30, validators = [validate_string]) email = forms.EmailField(required=False) address = forms.CharField(widget=forms.Textarea, required=False) city = forms.CharField(required=False) state = forms.CharField(required=False, validators = [validate_string]) country = forms.CharField(required=False) number = forms.CharField(max_length=10, validators = [validate_number]) def save(self): if self.is_valid(): data = self.cleaned_data person = Person.objects.create(first_name=data.get('first_name'), last_name=data.get('last_name'), email=data.get('email'), address=data.get('address'), city=data.get('city'), state=data.get('state'), country=data.get('country')) phone = Phone.objects.create(person=person, number=data.get('number')) return phone
-
再次運行測試,7個測試會通過的。
-
現在,先脫離開TDD一會兒。我想在客戶端添加一個額外的測試驗證。所以添加test_contact_form.py:
from django.test import TestCasefrom user_contacts.models import Personfrom user_contacts.new_contact_form import ContactFormclass TestContactForm(TestCase): def test_if_valid_contact_is_saved(self): form = ContactForm({'first_name':'test', 'last_name':'test','number':'9999900000'}) contact = form.save() self.assertEqual(contact.person.first_name, 'test') def test_if_invalid_contact_is_not_saved(self): form = ContactForm({'first_name':'tes&t', 'last_name':'test','number':'9999900000'}) contact = form.save() self.assertEqual(contact, None)
-
運行測試,所有9個測試都通過了。耶!現在可以提交代碼了。
功能測試的終極版
當單元測試已經完成了,我們現在添加功能測試去保證應用程序可以順利運行。但愿由于我們的單元測試已經通過了,功能測試也不會有什么問題。
-
添加一個新類到“tests.py”文件中:
class UserContactTest(LiveServerTestCase): def setUp(self): self.browser = webdriver.Firefox() self.browser.implicitly_wait(3) def tearDown(self): self.browser.quit() def test_create_contact(self): # user opens web browser, navigates to home page self.browser.get(self.live_server_url + '/') # user clicks on the Persons link add_link = self.browser.find_elements_by_link_text('Add Contact') add_link[0].click() # user fills out the form self.browser.find_element_by_name('first_name').send_keys("Michael") self.browser.find_element_by_name('last_name').send_keys("Herman") self.browser.find_element_by_name('email').send_keys("michael@realpython.com") self.browser.find_element_by_name('address').send_keys("2227 Lexington Ave") self.browser.find_element_by_name('city').send_keys("San Francisco") self.browser.find_element_by_name('state').send_keys("CA") self.browser.find_element_by_name('country').send_keys("United States") self.browser.find_element_by_name('number').send_keys("4158888888") # user clicks the save button self.browser.find_element_by_css_selector("input[value='Add']").click() # the Person has been added body = self.browser.find_element_by_tag_name('body') self.assertIn('michael@realpython.com', body.text) def test_create_contact_error(self): # user opens web browser, navigates to home page self.browser.get(self.live_server_url + '/') # user clicks on the Persons link add_link = self.browser.find_elements_by_link_text('Add Contact') add_link[0].click() # user fills out the form self.browser.find_element_by_name('first_name').send_keys("test@") self.browser.find_element_by_name('last_name').send_keys("tester") self.browser.find_element_by_name('email').send_keys("test@tester.com") self.browser.find_element_by_name('address').send_keys("2227 Tester Ave") self.browser.find_element_by_name('city').send_keys("Tester City") self.browser.find_element_by_name('state').send_keys("TC") self.browser.find_element_by_name('country').send_keys("TCA") self.browser.find_element_by_name('number').send_keys("4158888888") # user clicks the save button self.browser.find_element_by_css_selector("input[value='Add']").click() body = self.browser.find_element_by_tag_name('body') self.assertIn('Invalid', body.text)
-
運行功能測試:
$ python manage.py test ft
-
這里我們只測試我們寫過的,以及從最終用戶角度來看已經被單元測試過的代碼。4個測試都將會通過。
-
最后,我們通過添加以下功能到AdminTest類來保證我們添加進去的驗證會應用到管理員面板中:
def test_create_contact_admin_raise_error(self): # # user opens web browser, navigates to admin page, and logs in self.browser.get(self.live_server_url + '/admin/') username_field = self.browser.find_element_by_name('username') username_field.send_keys('admin') password_field = self.browser.find_element_by_name('password') password_field.send_keys('admin') password_field.send_keys(Keys.RETURN) # user clicks on the Persons link persons_links = self.browser.find_elements_by_link_text('Persons') persons_links[0].click() # user clicks on the Add person link add_person_link = self.browser.find_element_by_link_text('Add person') add_person_link.click() # user fills out the form self.browser.find_element_by_name('first_name').send_keys("test@") self.browser.find_element_by_name('last_name').send_keys("tester") self.browser.find_element_by_name('email').send_keys("test@tester.com") self.browser.find_element_by_name('address').send_keys("2227 Tester Ave") self.browser.find_element_by_name('city').send_keys("Tester City") self.browser.find_element_by_name('state').send_keys("TC") self.browser.find_element_by_name('country').send_keys("TCA") # user clicks the save button self.browser.find_element_by_css_selector("input[value='Save']").click() body = self.browser.find_element_by_tag_name('body') self.assertIn('Invalid', body.text)
-
運行它。會有5個測試通過。提交之后就可以收工啦。
測試結構
TDD是一個強大的工具以及是開發周期的一部分,幫助開發人員將程序拆分成小的、可讀性強的部分。這樣的組成部分可以更容易編寫和修改。另外,有一套全面完整的測試組件,覆蓋了你代碼的所有功能,有助于確保新功能在實現的時候不會破壞現有的功能。
在這過程中,功能測試是一個高層次的測試,重點放在了最終用戶的交互功能上。
同時,單元測試支持功能測試來測試代碼的每個功能。請記住,因為單元測試一次僅需測一個產品特征,所以它們更容易編寫,一般覆蓋性會更好些,也更容易調試。它們會運行非常快,所以你進行單元測試的次數往往會多于功能測試。
讓我們來看看我們的測試結構,看看我們的單元測試是如何支持功能測試的:
總結
恭喜你,你完成了!接下來做什么呢?
首先,我沒有100%地遵循TDD過程,這是沒有關系的。大部分用TDD進行開發的開發人員并不會始終堅持在每一個情況下都使用它。有時候,你為了把事情做好而偏離它這個過程——這是完全沒有問題的。如果你想重構代碼、過程使得它更好地遵循TDD過程,你也可以這么去做。事實上,這是一個很好的做法。
其次,思考一下我錯過的測試。確定什么地方以及什么時候去測試是困難的。這一般需要時間和大量的練習去把測試做好。我打算在我的下一篇文章中多留一些空白,來看看你們能否找到那些空白并添加測試。
最后,還記得TDD過程的最后一步嗎?這一步是至關重要的,因為它可以幫助創建可讀性強的、可維護的代碼,你不僅僅要現在理解這件事,在將來也要如此。當你重新看回你的代碼,思考下你結合起來的測試。此外,你應該添加哪些測試來確保所有寫過的代碼都被測試?例如你可以測試空值或者服務端的驗證。你也可以在準備寫新代碼前去重構之前沒時間去整理的代碼。或許這是另外一篇博文?思考下糟糕的代碼如何污染整個過程?
感謝閱讀。點擊這里獲取最終的代碼。有任何的問題請在下面評論。