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

测试驱动开发(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 <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”:
   

代码如下:

(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 href="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet" media="screen">
    <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 href="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet" media="screen"><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过程的最后一步吗?这一步是至关重要的,因为它可以帮助创建可读性强的、可维护的代码,你不仅仅要现在理解这件事,在将来也要如此。当你重新看回你的代码,思考下你结合起来的测试。此外,你应该添加哪些测试来确保所有写过的代码都被测试?例如你可以测试空值或者服务端的验证。你也可以在准备写新代码前去重构之前没时间去整理的代码。或许这是另外一篇博文?思考下糟糕的代码如何污染整个过程?

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

(0)

相关推荐

  • 详解Python的Django框架中的通用视图

    通用视图 1. 前言 回想一下,在Django中view层起到的作用是相当于controller的角色,在view中实施的 动作,一般是取得请求参数,再从model中得到数据,再通过数据创建模板,返回相应 响应对象.但在一些比较通用的功能中,比如显示对象列表,显示某对象信息,如果反复 写这么多流程的代码,也是一件浪费时间的事,在这里,Django同样给我们提供了类似的 "shortcut"捷径--通用视图. 2. 使用通用视图 使用通用视图的方法就是在urls.py这个路径配置文件中进

  • Django imgareaselect手动剪切头像实现方法

    本文实例讲述了Django imgareaselect手动剪切头像的方法.分享给大家供大家参考.具体如下: index.html: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>上传图片</title> </head> <body> <form action="." method="

  • Python使用django获取用户IP地址的方法

    本文实例讲述了Python使用django获取用户IP地址的方法.分享给大家供大家参考.具体如下: 函数实现: def get_client_ip(request): try: real_ip = request.META['HTTP_X_FORWARDED_FOR'] regip = real_ip.split(",")[0] except: try: regip = request.META['REMOTE_ADDR'] except: regip = "" r

  • Django中实现点击图片链接强制直接下载的方法

    本文实例讲述了Django中实现点击图片链接强制直接下载的方法.分享给大家供大家参考.具体分析如下: 当用户点击图片连接时,默认为在浏览器中直接开打图片,这段代码可以让图片链接变成下载 这段代码也非常适合下载大文件,基本不会消耗内存,每次只读取一部分数据到内存,然后提供下载 def Download(request): def readFile(fn, buf_size=262144): f = open(fn, "rb") while True: c = f.read(buf_siz

  • Django中几种重定向方法

    这里使用的是django1.5 需求: 有一个界面A,其中有一个form B, 前台提交B之后,后台保存数据之后,返回界面A,如果保存失败需要在A界面提示错误. 这里就需要后台的重定向,而且需要可以带着参数,也就是error message 这里收集了几种方法,简答说下需要那些包,怎么简单使用. 一. 使用HttpResponseRedirect The first argument to the constructor is required – the path to redirect to

  • Python中DJANGO简单测试实例

    本文实例讲述了Python中DJANGO简单测试的用法.分享给大家供大家参考.具体如下: 这里以facebook台湾的测试版为例. 仅仅测试用户登录,主要说明测试的使用和django环境的设置. 代码如下: import os import sys import unittest import hashlib TEST_MEMBER_ID = 11 SNS_ID = 100002309745702 TEST_SESSION_KEY = '125737724171219|2.AQCp7ctCYXJ

  • Django实现图片文字同时提交的方法

    本文实例讲述了Django实现图片文字同时提交的方法.分享给大家供大家参考.具体分析如下: jQuery为我们网站开发解决了很多问题,使我们的网站用户体验大大的提高了.举个简单的例子,我们用AJAX技术来实现对表单的异步提交,使用户在体验上有了很大的改观,用户在提交数据的同时还可以干一些其他的事情. 不过,今天在开发中遇到一个特别头痛的问题,刚开始不知道,以为可以实现,纠结了将近4个小时之久,但结果很是令人失望. 问题是这样的:为了提高用户体验,我决定使用AJAX异步提交,于是我用jQuery的

  • Django中使用group_by的方法

    本文实例讲述了Django中使用group_by的方法.分享给大家供大家参考.具体分析如下: 在Django中怎样使用group_by语句呢?找了很多资料,都没有看到好的,在这里分享两种方法给大家: 首先,我们先建一个简单的模型. class Book(models.Model): name = models.CharField(u'书名',max_length=255,db_index = True) author = models.CharField(u'作者',max_length=255

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

    测试驱动开发(TDD)是一个迭代的开发周期,强调编写实际代码之前编写自动化测试. 这个过程很简单: 先编写测试. 查看测试失败的地方 编写足够的代码以使测试通过. 再次测试. 代码重构 . 重复以上操作. 为什么要用TDD? 使用TDD,你将学会把你的代码拆分成符合逻辑的,简单易懂的片段,这有助于确保代码的正确性. 这一点非常重要,因为做到下面这些事情是非常困难的: 在我们的脑中一次性处理所有复杂的问题. 了解何时从哪里开始着手解决问题. 在代码库的复杂度不断增长的同时不引入错误和bug:并且

  • 使用url_helper简化Python中Django框架的url配置教程

    django的url采用正则表达式进行配置,虽然强大却也广为诟病.反对者们认为django的url配置过于繁琐,且不支持默认的路由功能. 我倒觉得还好,只是如果觉得不爽,为什么不自己小小的hack一下,反正也就几行代码的事. 在这个背景下,我整了这个url_helper,利用url_helper可以简化配置和实现url的默认路由.所谓的url_helper其实就只有url_helper.py一个文件,使用的时候只想要import就可以. url_helper的具体用法请参考具体的例子: url_

  • Python中Django框架利用url来控制登录的方法

    本文实例讲述了Python中Django框架利用url来控制登录的方法.分享给大家供大家参考.具体如下: from django.conf.urls.defaults import patterns,url #or use login_required from django.contrib.admin.views.decorators import staff_member_required def login_url(regex, view, *p,**args): """

  • python中django框架通过正则搜索页面上email地址的方法

    本文实例讲述了python中django框架通过正则搜索页面上email地址的方法.分享给大家供大家参考.具体实现方法如下: import re from django.shortcuts import render from pattern.web import URL, DOM, abs, find_urls def index(request): """ find email addresses in requested url or contact page &quo

  • Python及Django框架生成二维码的方法分析

    本文实例讲述了Python及Django框架生成二维码的方法.分享给大家供大家参考,具体如下: 一.包的安装和简单使用 1.1 用Python来生成二维码很简单,可以看 qrcode 这个包: pip install qrcode qrcode 依赖 Image 这个包: pip install Image 如果这个包安装有困难,可选纯Python的包来实现此功能,见下文. 1.2 安装后就可以使用了,这个程序带了一个 qr 命令: qr 'http://www.ziqiangxuetang.c

  • Python使用django框架实现多人在线匿名聊天的小程序

    最近看到好多设计类网站,都提供了多人在线匿名聊天的小功能,感觉很有意思,于是基于python的django框架自己写了一个,支持手动实时更名,最下方提供了完整的源码. 在线聊天地址(无需登录,开一个窗口,代表一个用户): http://zhaozhaoli.vicp.io/chatroom/happy/ 移动端聊天效果图: 网页版聊天效果图: 实现思路: 发送的消息通过ajax先写入数据库,通过ajax的循环请求,将写入数据库的消息显示到前端界面. 前端核心代码: <script> $(fun

  • Python的Django框架实现数据库查询(不返回QuerySet的方法)

    一.创建模型类: # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models class Course(models.Model): """课程表""" name = models.CharField(verbose_name='课程名称', max_length=255) description = models.Tex

  • Windows下安装Django框架的方法简明教程

    本文实例讲述了Windows下安装Django框架的方法.分享给大家供大家参考,具体如下: 在idea上运行Python项目时,出现了如下错误,这是因为系统中只安装了Python,没有安装Django,有童鞋可能会问,什么是Django,博主的建议是去问度年或谷老师 既然报错的原因就是缺少Django,那我们现在就开始安装这个应用框架. 本站下载Django1.6:Django-1.6.11.tar.gz 官网下载: Django-1.6.11.tar.gz || Django-1.7.11.t

  • python之django路由和视图案例教程

    django路由和视图 要了解django是如何运行的,首先要了解路由和视图两个概念,然后我们在项目中添加一些简单的路由和视图 路由和视图的概念 视图:一个视图函数(类),简称视图,是一个简单的Python一个视图函数(类),简称视图,是一个简单的Python 函数(类),它接受Web请求并且返回Web响应. 响应可以是一张网页的HTML内容,一个重定向,一个404错误,一个XML文档,或者一张图片. 无论视图本身包含什么逻辑,都要返回响应. 个人觉得django视图的概念其实是spring中的

  • Python ORM数据库框架Sqlalchemy的使用教程详解

    目录 概念和数据类型 安装 连接 创建数据库表类(模型) 生成数据库表 会话 增删改查 增 查 改 删 执行裸sql with关闭会话 sql建造者模式 封装的工具 数据库配置文件database.properties 工具 测试实体 验证代码 对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术.面向对象的开发方法是当今企业级应用开发环境中的主流开发方法,关系数据库是企业级应用环境中永久存放数据的主流数据

随机推荐