翻译于 2014/01/02 14:35
2 人 顶 此译文
Test Driven Development (TDD) is an iterative development cycle that emphasizes writing automated tests before writing the actual code.
The process is simple:
Write your tests first.
Watch them fail.
Write just enough code to make those tests pass.
Test again.
Refactor.
Repeat.
This is a long post, so here's a TOC for your convenience:
With TDD, you'll learn to break code up into logical, easily understandable pieces, helping ensure the correctness of code.
This is important because it's hard to-
Solve complex problems all at once in our heads;
Know when and where to start working on a problem;
Increase the complexity of a codebase without introducing errors and bugs; and
Recognize when code breaks occur.
TDD helps address these issues. It in no way guarantees that your code will be error free; however, you will write better code, which results in a better understanding of the code. This in itself will help with eliminating errors and at the very least, you will be able to address errors much easier.
TDD is practically an industry standard as well.
Enough talk. Let's get to the code.
For this tutorial, we'll be creating an app to store user contacts.
Please note: This tutorial assumes you are running a Unix-based environment - e.g, Mac OSX, straight Linux, or Linux VM through Windows. I will also be using Sublime 2 as my text editor. Also, make sure you you've completed the official Django tutorial and have a basic understanding of the Python language. Also, in this first post, we will not yet get into some of the new tools available in Django 1.6. This post sets the foundation for subsequent posts that deal with different forms of testing.
使用TDD,你将学会把你的代码拆分成符合逻辑的,简单易懂的片段,这有助于确保代码的正确性。
这一点非常重要,因为做到下面这些事情是非常困难的:
在我们的脑中一次性处理所有复杂的问题。
了解何时从哪里开始着手解决问题。
在代码库的复杂度不断增长的同时不引入错误和bug;并且
辨别出代码在什么时候发生了问题。
TDD帮助我们定位问题。它不能保证你的代码完全没有错误;然而,你可以写出更好的代码,从而能更好地理解理解代码。这本身有助于消除错误,并且至少,你可以更容易的定位错误。
TTD实际上也是一种行业标准。
说的够多了。让我们来看看代码吧。
在这个教程里,我们将创建一个存储用户联系人的app。
请注意: 这篇教程假设你运行在一个基于Unix的环境里 - 例如, Mac OSX, Linux, 或者在Windows下的Linux VM。 我将使用Sublime 2作为文本编辑器。并且,确保你已经完成了官方的Django教程并且基本了解Python语言. 此外,在这个第一篇post里,我们不会涉及到Django1.6提供的新工具。这篇文章将为之后的post打好基础来处理不同形式的测试。
Before we do anything we need to first setup a test. For this test we just want to make that Django is properly setup. We're going to be using a functional test for this - which we'll be explained further down.
Create a new directory to hold your project:
$ mkdir django-tdd $ cd django-tdd
Now setup a new directory to hold your functional tests:
$ mkdir ft $ cd ft
Create a new file called "tests.py" and add the following code:
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()
Now run the test:
$ python tests.py
Make sure you have selenium installed -
pip install selenium
You should see FireFox pop up and attempt to navigate to http://localhost:8000/. In your terminal you should see:
Traceback (most recent call last):File "tests.py", line 7, in <module>assert 'Django' in body.textAssertionError
Congrats! You wrote your first failing test.
Now let's write just enough code to make it pass, which simply amounts to setting up a Django development environment.
在开始做一些事情之前,我们需要首先创建一个测试。为了这个测试,我们需要让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 开发环境。
Activate a virtualenv:
$ cd ..$ virtualenv --no-site-packages env $ source env/bin/activate
Install Django and setup a Project:
$ pip install django==1.6.1$ django-admin.py startproject contacts
Your current project structure should look like this:
├── contacts│ ├── contacts│ │ ├── __init__.py│ │ ├── settings.py│ │ ├── urls.py│ │ └── wsgi.py│ └── manage.py└── ft └── tests.py
Install Selenium:
pip install selenium==2.39.0
Run the server:
$ cd contacts $ python manage.py runserver
Next, open up a new window in your terminal, navigate to the "ft" directory, then run the test again:
$ python tests.py
You should see the FireFox window navigate to http://localhost:8000/ again. This time there should be no errors. Nice. You just passed your first test!
Now, let's finish setting up our dev environment.
Version Control
First, add a ".gitignore" and include the following code in the file:
.Pythonenv bin lib include.DS_Store.pyc
Now create a Git repository and commit:
$ git init $ git add .$ git commit -am "initial"
With the project setup, let's take a step back and discuss functional tests.
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. 项目建完了,现在我们回头讨论一下功能测试吧。
We approached the first test through Selenium via Functional tests. Such tests let us drive the web browser as if we were the end user, to see how the application actually functions. Since these tests follow the behavior of the end user - also called a User Story - it involves the testing of a number of features, rather than just a single function - which is more appropriate for Unit tests. It's important to note that when testing code you have not written, you should begin with functional tests. Since we are essentially testing Django code, functional tests are the right way to go.
Another way to think about functional vs unit tests is that Functional tests focus on testing the app from the outside, from the user's perspective, while unit tests focus on the app from the inside, from the developer's perspective.
This will make much more sense in practice.
Before moving on, let's restructure our testing environment to make testing easier.
First, let's re-write the first test in the "tests.py" file:
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)
Then run it:
$ python manage.py test ft
It should pass:
----------------------------------------------------------------------Ran 1 test in 3.304sOK
Congrats!
Before moving on, let's see what's going on here. If all went well it should pass. You should also see FireFox open and go through the process we indicated in the test with the setUp()
and tearDown()
functions. The test itself is simply testing whether the "/admin" (self.browser.get(self.live_server_url + '/admin/'
) page can be found and that the words "Django administration" are present in the body tag.
Let's confirm this.
Run the server:
$ python manage.py runserver
Then navigate to http://localhost:8000/admin/ in your browser and you should see:
We can confirm that the test is working correctly by simply testing for the wrong thing. Update the last line in the test to:
self.assertIn('administration Django', body.text)
Run it again. You should see the following error (which is expected, of course):
AssertionError: 'administration Django' not found in u'Django administration\nUsername:\nPassword:\n '
Correct the test. Test it again. Commit the code.
Finally, did you notice that we started the function name for the actual test began with test_
. This is so that the Django test runner can find the test. In other words, any function that begins with test_
will be treated as a test by the test runner.
我们通过用 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_开头命名的功能都会被测试运行器视为一个测试。
Next, let's test to make sure the user can login to the admin site.
Update test_admin_site
the function in "tests.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)
So -
find_element_by_name
- is used for locating the input fields
send_keys
- sends keystrokes
Run the test. You should see this error:
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 '
This failed because we don't have an admin user setup. This is an expected failure, which is good. In other words, we knew it would fail - which makes it much easier to fix.
Sync the database:
$ python manage.py syncdb
Setup an admin user.
Test again. It should fail again. Why? Django creates a copy of our database when tests are ran so that way tests do not affect the production database.
We need to setup a Fixture, which is a file containing data we want loaded into the test database: the login credentials. To do that, run these commands to dump the admin user info from the database to the Fixture:
$ mkdir ft/fixtures $ python manage.py dumpdata auth.User --indent=2 > ft/fixtures/admin.json
Now update the AdminTest
class:
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)
Run the test. It should pass.
Each time a test is ran, Django dumps the test database. Then all the Fixtures specified in the "test.py" file are loaded into the database.
Let's add one more assert. Update the test again:
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)
Run it. It should fail, because we need to and another user to the fixture file:
[{"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"}}]
Run it again. Make sure it passes. Refactor the test if needed. Now think about what else you could test. Perhaps you could test to make sure the admin user can add a user in the Admin panel. Or perhaps a test to ensure that someone without admin access cannot access the Admin panel. Write a few more tests. Update your code. Test again. Refactor if necessary.
Next, we're going to add the app for adding contacts. Don't forget to commit!
接下来,让我们来测试,以确保用户可以登录到管理网站。
更新“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"}}]
再次运行,它是会通过的。如果需要可以重构一下这个测试。现在想想还有什么可以测试。或许你可以测试管理员用户可以添加一个用户到管理面板中,或者可以测试没有管理员权限的人是不能进入管理面板中。写几个测试,更新你的代码,再次测试,根据需要重构代码。
接下来,我们会添加增加联系人应用,不要忘了提交代码哦!
Start with a test. Add the following function:
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)
Run the test suite again. You should see the following error-
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'
-which is expected.
Now, write just enough code for this to pass.
Create the App:
$ python manage.py startapp user_contacts
Add it to the "settings.py" file:
INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'ft', 'user_contacts',)
Within the "admin.py" file in the user_contacts
directory add the following code:
from user_contacts.models import Person, Phonefrom django.contrib import admin admin.site.register(Person)admin.site.register(Phone)
Your project structure should now look like this:
.├── 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
Update "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
Run the test again now. You should now see:
Ran 2 tests in 11.730sOK
Let's go ahead and add to the test to make sure the admin can add data:
# user clicks on the Persons linkpersons_links = self.browser.find_elements_by_link_text('Persons')persons_links[0].click()# user clicks on the Add person linkadd_person_link = self.browser.find_element_by_link_text('Add person')add_person_link.click()# user fills out the formself.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 buttonself.browser.find_element_by_css_selector("input[value='Save']").click()# the Person has been addedbody = self.browser.find_element_by_tag_name('body')self.assertIn('Herman, Michael', body.text)# user returns to the main admin screenhome_link = self.browser.find_element_by_link_text('Home')home_link.click()# user clicks on the Phones linkpersons_links = self.browser.find_elements_by_link_text('Phones')persons_links[0].click()# user clicks on the Add phone linkadd_person_link = self.browser.find_element_by_link_text('Add phone')add_person_link.click()# user finds the person in the dropdownel = 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 numbersself.browser.find_element_by_name('number').send_keys("4158888888")# user clicks the save buttonself.browser.find_element_by_css_selector("input[value='Save']").click()# the Phone has been addedbody = self.browser.find_element_by_tag_name('body')self.assertIn('4158888888', body.text)# user logs outself.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)
That's it for the admin functionality. Let's switch gears and focus on the application, user_contacts
, itself. Did you forget to commit? If so, do it now.
开始一个测试,添加以下功能:
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本身。你之前的代码还记得提交吗?如果没有,赶紧提交吧!
Think about the features we have written thus far. We've just defined our model and allowed admins to alter the model. Based on that, and the overall goal of our Project, focus on the remaining user functionalities.
Users should be able to-
View all contacts.
Add new contacts.
Try to formulate the remaining Functional test(s) based on those requirements. Before we write the functional tests, though, we should define the behavior of the code through unit tests - which will help you write good, clean code, making it easier to write the Functional tests.
Remember: Functional tests are the ultimate indicator of whether your Project works or not, while Unit tests are the means to help you reach that end. This will all make sense soon.
Let's pause for a minute and talk about some conventions.
Although the basics of TDD (or ends) - test, code, refactor - are universal, many developers approach the means differently. For example, I like to write my unit tests first, to ensure that my code works at a granular level, then write the functional tests. Others write functional tests first, watch them fail, then write unit tests, watch them fail, then write code to first satisfy the unit tests, which should ultimately satisfy the functional tests. There's no right or wrong answer here. Do what feels most comfortable - but continue to test first, then write code, and finally refactor.
单元测试
考虑下我们现在已经写的特性。我们已经定义了我们的模型,允许管理员更改模型。根据这个情况和我们项目的整体目标,着重关注剩下的用户功能。
用户应该可以——
浏览所有的联系人。
添加新的联系人。
根据这些需求,尝试把剩下的功能测试公式化。尽管,在我们写功能测试之前,我们应该通过单元测试定义代码的行为——这有助于你写出良好、干净的代码,编写功能测试更加简单。
记住:功能测试最终将表示你的项目是否工作,而单元测试有助于你达到这样的目的。这很快就会变的有意义。
让我们暂停片刻,谈论一些常规惯例。
尽管TDD(或者终端)的基础——测试、代码、重构——是通用的,很多开发者使用的方法是不同的。例如,我喜欢先写单元测试,保证我们的代码在细粒度级别有效,然后写功能测试。其他开发者先写功能测试,查看它们失败,然后写单元测试,查看它们失败,然后再写代码,首先满足单元测试,最终也应该满足功能测试。这里没有正确和错误的答案。哪种方法舒服用哪种——但继续先测试、然后写代码,最后重构。
First, check to make sure all the views are setup correctly.
As always, start with a test:
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)
Name this test test_views.py
and save it in the user_contacts/tests
directory. Also add an __init__.py
file to the directory and delete the "tests.py" file in the main user_contacts
directory.
Run it:
$ python manage.py test user_contacts
It should fail - AssertionError: 404 != 200
- because the URL, View, and the Template do not exist. If you're unfamiliar with how Django handles the MVC architecture, please read the short article here.
The test is simple. We first GET the url "/" using the Client, which is part of Django’s TestCase
. The response is stored, then we check to make sure the returned status code is equal to 200.
Add the following route to "contacts/urls.py":
url(r'^', include('user_contacts.urls')),
Update "user_contacts/urls.py":
from django.conf.urls import patterns, urlfrom user_contacts.views import *urlpatterns = patterns('', url(r'^$', home),)
Update "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')
Add an "index.html" template to the templates directory:
<!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>
Run the test again. It should pass just fine.
首先,检查所有视图都设置准确。
跟往常一样,先开始一个测试:
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”:
url(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>
再次运行测试,它就会顺利通过。
The test for this view is nearly identical to our last test. Try it on your own before looking at my answer.
Write the test first by adding the following function to the ViewTest
class:
def test_view_contacts_route(self): response = self.client_stub.get('/all/') self.assertEquals(response.status_code, 200)
When ran, you should see the same error: AssertionError: 404 != 200
.
Update "user_contacts/urls.py" with the following route:
url(r'^all/$', all_contacts),
Update "views.py":
def all_contacts(request): contacts = Phone.objects.all() return render_to_response('all.html', {'contacts':contacts})
Add an "all.html" template to the templates directory:
<!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>
This should pass as well.
This test is slightly different from the previous two, so please follow along closely.
Add the test to the test suite:
def test_add_contact_route(self): response = self.client_stub.get('/add/') self.assertEqual(response.status_code, 200)
You should see this error when ran: AssertionError: 404 != 200
Update "urls.py":
url(r'^add/$', add),
Update "views.py":
def add(request):person_form = ContactForm()return render(request, 'add.html', {'person_form' : person_form}, context_instance = RequestContext(request))
Make sure to add the following import:
from user_contacts.new_contact_form import ContactForm
Create a new file called new_contact_form.py
and add the following code:
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
Add "add.html" to the templates directory:
<!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>Add Contact</h1> <br> <form action="/create" method ="POST" role="form"> {% csrf_token %} <input type ="submit" name ="Submit" class="btn btn-default" value ="Add"> </form> <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>
Does it pass? It should. If not, refactor.
所有联系人视图
对这个视图的测试几乎和我们上一个测试相同。在看我的答案之前先自己试试吧。
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. 是不是通过了?应该是的。如果没有,再检查一下。