再次运行测试,它就会顺利通过。
所有联系人视图
对这个视图的测试几乎和我们上一个测试相同。在看我的答案之前先自己试试吧。
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"的模板:
All Contacts.All Contacts
First Name |
Last Name |
Address |
City |
State |
Country |
Phone Number |
Email |
---|
{% for contact in contacts %}
|
|
|
|
|
|
|
|
{% endfor %}
Return Home6. 然后测试应该能通过了。
增加联系人视图
这个测试与前面两个稍有不同,所以一定要仔细的跟着下列步骤走。
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过程的最后一步吗?这一步是至关重要的,因为它可以帮助创建可读性强的、可维护的代码,你不仅仅要现在理解这件事,在将来也要如此。当你重新看回你的代码,思考下你结合起来的测试。此外,你应该添加哪些测试来确保所有写过的代码都被测试?例如你可以测试空值或者服务端的验证。你也可以在准备写新代码前去重构之前没时间去整理的代码。或许这是另外一篇博文?思考下糟糕的代码如何污染整个过程?
感谢阅读。点击这里获取最终的代码。有任何的问题请在下面评论。