好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

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

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

这个过程很简单:

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

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

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

代码如下:


├── 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”:

代码如下:


(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”模板到模板目录中去:

 
    
    
     Welcome. 
     
    
     
      .container {
        padding: 50px;
      }
     
    
   
    

What would you like to do? View Contacts Add Contact

再次运行测试,它就会顺利通过。


所有联系人视图

对这个视图的测试几乎和我们上一个测试相同。在看我的答案之前先自己试试吧。

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.   
 .container {
  padding: 50px;
 }  

All Contacts First Name Last Name Address City State Country Phone Number Email {% for contact in contacts %} {% endfor %}
Return Home

查看更多关于详尽讲述用Python的Django框架测试驱动开发的教程的详细内容...

  阅读:48次