目次
What would you like to do?
All Contacts
ホームページ バックエンド開発 Python チュートリアル 详尽讲述用Python的Django框架测试驱动开发的教程

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

Jun 10, 2016 pm 03:14 PM
python

测试驱动开发(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 までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

PSが荷重を見せ続ける理由は何ですか? PSが荷重を見せ続ける理由は何ですか? Apr 06, 2025 pm 06:39 PM

PSの「読み込み」の問題は、リソースアクセスまたは処理の問題によって引き起こされます。ハードディスクの読み取り速度は遅いか悪いです。CrystaldiskInfoを使用して、ハードディスクの健康を確認し、問題のあるハードディスクを置き換えます。不十分なメモリ:高解像度の画像と複雑な層処理に対するPSのニーズを満たすためのメモリをアップグレードします。グラフィックカードドライバーは時代遅れまたは破損しています:ドライバーを更新して、PSとグラフィックスカードの間の通信を最適化します。ファイルパスが長すぎるか、ファイル名に特殊文字があります。短いパスを使用して特殊文字を避けます。 PS独自の問題:PSインストーラーを再インストールまたは修理します。

PSが開始されたときにロードの問題を解決する方法は? PSが開始されたときにロードの問題を解決する方法は? Apr 06, 2025 pm 06:36 PM

ブートがさまざまな理由によって引き起こされる可能性がある場合、「読み込み」に巻き込まれたPS:腐敗したプラグインまたは競合するプラグインを無効にします。破損した構成ファイルの削除または名前変更。不十分なプログラムを閉じたり、メモリをアップグレードしたりして、メモリが不十分であることを避けます。ソリッドステートドライブにアップグレードして、ハードドライブの読み取りをスピードアップします。 PSを再インストールして、破損したシステムファイルまたはインストールパッケージの問題を修復します。エラーログ分析の起動プロセス中にエラー情報を表示します。

PSがファイルを開いたときにロードの問題を解決する方法は? PSがファイルを開いたときにロードの問題を解決する方法は? Apr 06, 2025 pm 06:33 PM

「ロード」は、PSでファイルを開くときに発生します。理由には、ファイルが大きすぎるか破損しているか、メモリが不十分で、ハードディスクの速度が遅い、グラフィックカードドライバーの問題、PSバージョンまたはプラグインの競合が含まれます。ソリューションは、ファイルのサイズと整合性を確認し、メモリの増加、ハードディスクのアップグレード、グラフィックカードドライバーの更新、不審なプラグインをアンインストールまたは無効にし、PSを再インストールします。この問題は、PSパフォーマンス設定を徐々にチェックして使用し、優れたファイル管理習慣を開発することにより、効果的に解決できます。

インストール後にMySQLの使用方法 インストール後にMySQLの使用方法 Apr 08, 2025 am 11:48 AM

この記事では、MySQLデータベースの操作を紹介します。まず、MySQLWorkBenchやコマンドラインクライアントなど、MySQLクライアントをインストールする必要があります。 1. mysql-uroot-pコマンドを使用してサーバーに接続し、ルートアカウントパスワードでログインします。 2。CreatedAtaBaseを使用してデータベースを作成し、データベースを選択します。 3. createTableを使用してテーブルを作成し、フィールドとデータ型を定義します。 4. INSERTINTOを使用してデータを挿入し、データをクエリし、更新することでデータを更新し、削除してデータを削除します。これらの手順を習得することによってのみ、一般的な問題に対処することを学び、データベースのパフォーマンスを最適化することでMySQLを効率的に使用できます。

PSフェザーリングは、遷移の柔らかさをどのように制御しますか? PSフェザーリングは、遷移の柔らかさをどのように制御しますか? Apr 06, 2025 pm 07:33 PM

羽毛の鍵は、その漸進的な性質を理解することです。 PS自体は、勾配曲線を直接制御するオプションを提供しませんが、複数の羽毛、マッチングマスク、および細かい選択により、半径と勾配の柔らかさを柔軟に調整して、自然な遷移効果を実現できます。

mysqlは支払う必要がありますか mysqlは支払う必要がありますか Apr 08, 2025 pm 05:36 PM

MySQLには、無料のコミュニティバージョンと有料エンタープライズバージョンがあります。コミュニティバージョンは無料で使用および変更できますが、サポートは制限されており、安定性要件が低く、技術的な能力が強いアプリケーションに適しています。 Enterprise Editionは、安定した信頼性の高い高性能データベースを必要とするアプリケーションに対する包括的な商業サポートを提供し、サポートの支払いを喜んでいます。バージョンを選択する際に考慮される要因には、アプリケーションの重要性、予算編成、技術スキルが含まれます。完璧なオプションはなく、最も適切なオプションのみであり、特定の状況に応じて慎重に選択する必要があります。

MySQLインストール後にデータベースのパフォーマンスを最適化する方法 MySQLインストール後にデータベースのパフォーマンスを最適化する方法 Apr 08, 2025 am 11:36 AM

MySQLパフォーマンスの最適化は、インストール構成、インデックス作成、クエリの最適化、監視、チューニングの3つの側面から開始する必要があります。 1。インストール後、INNODB_BUFFER_POOL_SIZEパラメーターやclose query_cache_sizeなど、サーバーの構成に従ってmy.cnfファイルを調整する必要があります。 2。過度のインデックスを回避するための適切なインデックスを作成し、説明コマンドを使用して実行計画を分析するなど、クエリステートメントを最適化します。 3. MySQL独自の監視ツール(ShowProcessList、ShowStatus)を使用して、データベースの健康を監視し、定期的にデータベースをバックアップして整理します。これらの手順を継続的に最適化することによってのみ、MySQLデータベースのパフォーマンスを改善できます。

PSカードがロードインターフェイスにある場合はどうすればよいですか? PSカードがロードインターフェイスにある場合はどうすればよいですか? Apr 06, 2025 pm 06:54 PM

PSカードの読み込みインターフェイスは、ソフトウェア自体(ファイルの破損またはプラグインの競合)、システム環境(ドライバーまたはシステムファイルの破損)、またはハードウェア(ハードディスクの破損またはメモリスティックの障害)によって引き起こされる場合があります。まず、コンピューターリソースで十分かどうかを確認し、バックグラウンドプログラムを閉じ、メモリとCPUリソースをリリースします。 PSのインストールを修正するか、プラグインの互換性の問題を確認してください。 PSバージョンを更新またはフォールバックします。グラフィックカードドライバーをチェックして更新し、システムファイルチェックを実行します。上記の問題をトラブルシューティングする場合は、ハードディスク検出とメモリテストを試すことができます。

See all articles