搜索
首页后端开发Python教程详尽讲述用Python的Django框架测试驱动开发的教程

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

这个过程很简单:

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

2015422121310993.png (407×401)

为什么要用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/ 你会看到:

2015422121333572.png (862×399)

        我们可以只需对错误的东西进行简单地测试便能确认测试是否正确运作。更新测试里的最后一行:
   

  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 id="What-would-you-like-to-do">What would you like to do&#63;</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 id="All-Contacts">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是一个强大的工具以及是开发周期的一部分,帮助开发人员将程序拆分成小的、可读性强的部分。这样的组成部分可以更容易编写和修改。另外,有一套全面完整的测试组件,覆盖了你代码的所有功能,有助于确保新功能在实现的时候不会破坏现有的功能。

在这过程中,功能测试是一个高层次的测试,重点放在了最终用户的交互功能上。

同时,单元测试支持功能测试来测试代码的每个功能。请记住,因为单元测试一次仅需测一个产品特征,所以它们更容易编写,一般覆盖性会更好些,也更容易调试。它们会运行非常快,所以你进行单元测试的次数往往会多于功能测试。

让我们来看看我们的测试结构,看看我们的单元测试是如何支持功能测试的:

2015422121427964.png (982×304)

总结

恭喜你,你完成了!接下来做什么呢?

首先,我没有100%地遵循TDD过程,这是没有关系的。大部分用TDD进行开发的开发人员并不会始终坚持在每一个情况下都使用它。有时候,你为了把事情做好而偏离它这个过程——这是完全没有问题的。如果你想重构代码、过程使得它更好地遵循TDD过程,你也可以这么去做。事实上,这是一个很好的做法。

其次,思考一下我错过的测试。确定什么地方以及什么时候去测试是困难的。这一般需要时间和大量的练习去把测试做好。我打算在我的下一篇文章中多留一些空白,来看看你们能否找到那些空白并添加测试。

最后,还记得TDD过程的最后一步吗?这一步是至关重要的,因为它可以帮助创建可读性强的、可维护的代码,你不仅仅要现在理解这件事,在将来也要如此。当你重新看回你的代码,思考下你结合起来的测试。此外,你应该添加哪些测试来确保所有写过的代码都被测试?例如你可以测试空值或者服务端的验证。你也可以在准备写新代码前去重构之前没时间去整理的代码。或许这是另外一篇博文?思考下糟糕的代码如何污染整个过程?

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

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
Python vs. C:了解关键差异Python vs. C:了解关键差异Apr 21, 2025 am 12:18 AM

Python和C 各有优势,选择应基于项目需求。1)Python适合快速开发和数据处理,因其简洁语法和动态类型。2)C 适用于高性能和系统编程,因其静态类型和手动内存管理。

Python vs.C:您的项目选择哪种语言?Python vs.C:您的项目选择哪种语言?Apr 21, 2025 am 12:17 AM

选择Python还是C 取决于项目需求:1)如果需要快速开发、数据处理和原型设计,选择Python;2)如果需要高性能、低延迟和接近硬件的控制,选择C 。

达到python目标:每天2小时的力量达到python目标:每天2小时的力量Apr 20, 2025 am 12:21 AM

通过每天投入2小时的Python学习,可以有效提升编程技能。1.学习新知识:阅读文档或观看教程。2.实践:编写代码和完成练习。3.复习:巩固所学内容。4.项目实践:应用所学于实际项目中。这样的结构化学习计划能帮助你系统掌握Python并实现职业目标。

最大化2小时:有效的Python学习策略最大化2小时:有效的Python学习策略Apr 20, 2025 am 12:20 AM

在两小时内高效学习Python的方法包括:1.回顾基础知识,确保熟悉Python的安装和基本语法;2.理解Python的核心概念,如变量、列表、函数等;3.通过使用示例掌握基本和高级用法;4.学习常见错误与调试技巧;5.应用性能优化与最佳实践,如使用列表推导式和遵循PEP8风格指南。

在Python和C之间进行选择:适合您的语言在Python和C之间进行选择:适合您的语言Apr 20, 2025 am 12:20 AM

Python适合初学者和数据科学,C 适用于系统编程和游戏开发。1.Python简洁易用,适用于数据科学和Web开发。2.C 提供高性能和控制力,适用于游戏开发和系统编程。选择应基于项目需求和个人兴趣。

Python与C:编程语言的比较分析Python与C:编程语言的比较分析Apr 20, 2025 am 12:14 AM

Python更适合数据科学和快速开发,C 更适合高性能和系统编程。1.Python语法简洁,易于学习,适用于数据处理和科学计算。2.C 语法复杂,但性能优越,常用于游戏开发和系统编程。

每天2小时:Python学习的潜力每天2小时:Python学习的潜力Apr 20, 2025 am 12:14 AM

每天投入两小时学习Python是可行的。1.学习新知识:用一小时学习新概念,如列表和字典。2.实践和练习:用一小时进行编程练习,如编写小程序。通过合理规划和坚持不懈,你可以在短时间内掌握Python的核心概念。

Python与C:学习曲线和易用性Python与C:学习曲线和易用性Apr 19, 2025 am 12:20 AM

Python更易学且易用,C 则更强大但复杂。1.Python语法简洁,适合初学者,动态类型和自动内存管理使其易用,但可能导致运行时错误。2.C 提供低级控制和高级特性,适合高性能应用,但学习门槛高,需手动管理内存和类型安全。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

VSCode Windows 64位 下载

VSCode Windows 64位 下载

微软推出的免费、功能强大的一款IDE编辑器

MinGW - 适用于 Windows 的极简 GNU

MinGW - 适用于 Windows 的极简 GNU

这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

mPDF

mPDF

mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )专业的PHP集成开发工具

SublimeText3 英文版

SublimeText3 英文版

推荐:为Win版本,支持代码提示!