• 欢迎访问搞代码网站,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站!
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏搞代码吧

详尽讲述用Python的Django框架测试驱动开发的教程

python 搞代码 4年前 (2022-01-09) 29次浏览 已收录 0个评论

测试驱动开发(TDD)是一个迭代的开发周期,强调编写实际代码之前编写自动化测试。

这个过程很简单:

  1. 先编写测试。
  2. 查看测试失败的地方
  3. 编写足够的代码以使测试通过。
  4. 再次测试。
  5. 代码重构 。
  6. 重复以上操作。

为什么要用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 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

你当前的项目结构应该是下面这个样子:

<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&gt;

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':'[email protected]', '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':'[email protected]', '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):<strong style="color:transparent">本文来源gao@daima#com搞(%代@#码网@</strong>   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("[email protected]")     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('[email protected]', 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("[email protected]")     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("[email protected]")   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过程的最后一步吗?这一步是至关重要的,因为它可以帮助创建可读性强的、可维护的代码,你不仅仅要现在理解这件事,在将来也要如此。当你重新看回你的代码,思考下你结合起来的测试。此外,你应该添加哪些测试来确保所有写过的代码都被测试?例如你可以测试空值或者服务端的验证。你也可以在准备写新代码前去重构之前没时间去整理的代码。或许这是另外一篇博文?思考下糟糕的代码如何污染整个过程?

感谢阅读。点击这里获取最终的代码。有任何的问题请在下面评论。


搞代码网(gaodaima.com)提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发送到邮箱[email protected],我们会在看到邮件的第一时间内为您处理,或直接联系QQ:872152909。本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:详尽讲述用Python的Django框架测试驱动开发的教程

喜欢 (0)
[搞代码]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址