文章目录
- 第一部分 TDD和Django基础
- 第1章 使用功能测试协助安装Django
- (1) 让Django运行起来
- (2)创建git仓库
- 第2章 使用unittest模块拓展功能测试
- (1)unitttest模块的使用
- 第3章 使用单元测试测试简单的首页
- (1)第一个Django应用,第一个单元测试
- (2)Django中的mvc,url和视图函数
- 第4章:编写这些测试有什么用
第一部分 TDD和Django基础
第1章 使用功能测试协助安装Django
(1) 让Django运行起来
- 创建文件:functional_tests.py
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('http://localhost:8000')
assert 'Django' in browser.title
- 创建目录:django-admin startproject superlists
----functional_tests.py
----superlists
---manage.py
---superlists
---__init__.py
---settings.py
---urls.py
---wsgi.py
- 运行服务器:python manage.py runserver
- 打开另外终端窗口,运行:python functional_tests.py
运行functional_tests.py的前提是已经安装selenium关于浏览器的驱动(本机中只有chrome,因此下载对应驱动,且需要参照chrome的版本下载)
查看python库的版本命令:pip list;
(2)创建git仓库
# 查看当前目录下文件
ls
# 将functional_tests.py移动到superlists目录下
mv functional_tests.py superlists/
#进入superlists目录
cd superlists
#创建仓库
git init .
# 查看所有文件
ls
#忽略db.sqlite3,不将其纳入版本控制
echo "db.sqlite3" >> .gitignore
# 添加其他文件
git add .
# 查看状态
git status
'''
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: .gitignore
new file: db.sqlite3
new file: functional_tests.py
new file: manage.py
new file: superlists/__init__.py
new file: superlists/__pycache__/__init__.cpython-36.pyc
new file: superlists/__pycache__/settings.cpython-36.pyc
new file: superlists/__pycache__/urls.cpython-36.pyc
new file: superlists/__pycache__/wsgi.cpython-36.pyc
new file: superlists/settings.py
new file: superlists/urls.py
new file: superlists/wsgi.py
'''
# 删除.pyc文件
# git rm --cached从索引中删除文件,但本地文件还存在,不希望这个文件被版本控制
git rm -r --cached superlists/__pycache__
echo "__pycache__" >> .gitignore
echo ".pyc" >> .gitignore
#做第一次提交
git commit
'''
提示:
Author identity unknown
*** Please tell me who you are.
Run
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
to set your account's default identity.
Omit --global to set the identity only in this repository.
fatal: unable to auto-detect email address (got 'sophia@sophia.(none)')
'''
# 修改全局用户信息
git config --global user.name "sophia"
git config --global user.email sophia@example.com
'''
# 查看用户名和邮箱名称
git config user.name
git config user.email
'''
# 弹出编辑窗口,输入提交消息:First commit:First FT and basic
# Django config
第2章 使用unittest模块拓展功能测试
(1)unitttest模块的使用
- 在functional_tests.py中写入:
from selenium import webdriver
import unittest
class NewVisitorTest(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Chrome()
def tearDown(self):
self.browser.quit()
def test_can_start_a_list_and_retrieve_it_later(self):
self.browser.get("http://localhost:8000")
self.assertIn('To-Do',self.browser.title)
self.fail('Finish the test!')
if __name__ == '__main__':
unittest.main(warnings='ignore')
运行服务器:python manage.py runserver
另外打开终端运行文件:python funcitonal_tests.py
Traceback (most recent call last):
File "functional_tests.py", line 15, in test_can_start_a_list_and_retrieve_it_later
self.assertIn('To-Do',self.browser.title)
AssertionError: 'To-Do' not found in 'Django: the Web framework for perfectionists with deadlines.'
第3章 使用单元测试测试简单的首页
(1)第一个Django应用,第一个单元测试
创建一个Django应用:
python manage.py startapp lists
在lists/tests.py中写入:
class SmokeTest(TestCase):
def test_bad_maths(self):
self.assertEqual(1+1,3)
运行:python manage.py test
Traceback (most recent call last):
File "D:\study-day-day-up!\练习项目\TDDTest\ch01\superlists\lists\tests.py", line 8, in test_bad_maths
self.assertEqual(1+1,3)
AssertionError: 2 != 3
查看状态和变化并提交:
git status
git add lists
git diff --staged
# git commit -m 中-m可以直接写提交信息,不用在编辑器中编辑
git commit -m "Add app for lists,with deliberately failing unit test"
(2)Django中的mvc,url和视图函数
Django测试网页目的:
- 能否解析网站根据路径("/")的url,将其对应到编写的某个视图函数上
- 能否让视图函数返回一些html,让功能测试通过
首先测试是否能够解析目录:
打开lists/tests.py,改写成:
from django.test import TestCase
# 书中为from django.core.urlresolvers import resolve
# 报错 No module name 'django.core.urlresolvers'
# 原因:django2.0 把原来的 django.core.urlresolvers 包 更改为了 django.urls包,所以我们需要把导入的包都修改一下就可以了。
from django.urls import resolve
from lists.views import home_page
# Create your tests here.
'''
class SmokeTest(TestCase):
def test_bad_maths(self):
self.assertEqual(1+1,3)
'''
class HomePageTest(TestCase):
def test_root_url_resolves_to_home_page_view(self):
found = resolve('/')
self.assertEqual(found.func,home_page)
终端结果:
$ python manage.py test
ImportError: cannot import name 'home_page'
修正问题:无法从lists.views中导入home_page
解决:在lists/views.py中写入:
from django.shortchts import render
home_page = None
再次运行测试:
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E
======================================================================
ERROR: test_root_url_resolves_to_home_page_view (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\study-day-day-up!\练习项目\TDDTest\ch01\superlists\lists\tests.py", line 17, in test_root_url_resolves_to_home_page_view
found = resolve('/')
File "D:\Anaconda3\envs\env1\lib\site-packages\django\urls\base.py", line 24, in resolve
return get_resolver(urlconf).resolve(path)
File "D:\Anaconda3\envs\env1\lib\site-packages\django\urls\resolvers.py", line 571, in resolve
raise Resolver404({'tried': tried, 'path': new_path})
django.urls.exceptions.Resolver404: {'tried': [[<URLResolver <URLPattern list> (admin:admin) 'admin/'>]], 'path': ''}
----------------------------------------------------------------------
Ran 1 test in 0.013s
FAILED (errors=1)
Destroying test database for alias 'default'...
错误原因分析:尝试解析"/“时,Django抛出404错误,即无法找到”/"的URL映射。
解决:在urls.py文件中定义如何把URL映射到视图函数上。
配置方法有两种:
- 直接在根urls.py文件(superlists/superlists/urls.py)中配置
from django.contrib import admin
from django.urls import path
from lists import views #导入应用中的视图文件
urlpatterns = [
#path('admin/', admin.site.urls), #暂时用不到后台条目,注释掉
path('',views.home_page,name='home'),
]
- 在应用中配置:在应用lists中创建子urls.py文件:
from django.urls import path
from . import views
urlpatterns=[
path('',views.home_page,name='home'),
]
然后导入到根urls.py文件中:
from django.contrib import admin
from django.urls import path,include
urlpatterns = [
#path('admin/', admin.site.urls),
path('',include('lists.urls')),
]
运行结果:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
需要注意的是,Django 2.x与之前版本在配置路径上略有区别,详情可见博客:
最后,提交版本信息:
# 添加所有已跟踪文件中的改动,而且使用命令行中输入的提交信息
git commit -am "First unit test url mapping,dummy view"
接下来为视图编写单元测试。
单元测试流程:
- 在终端运行单元测试(python manage.py test),看他们是如何失败的;
- 在编码器中改动最少量的代码,让当前失败的测试通过。
然后不断重复。
打开lists/tests.py,添加一个新测试方法。
from django.urls import resolve #此处和书中有出入,前面已经提及
from django.test import TestCase
from django.http import HttpRequest
from lists.views import home_page
class Home PageTest(TestCase):
def test_root_url_resolves_to_home_page_view(self):
found = resolve('/')
self.assertEqual(found.func,home_page)
def test_home_page_returns_correct_html(self):
request = HttpRequest()
response = home_page(request)
self.assertTrue(response.content.startswith(b'<html>'))
self.assertIn(b'<title>To-Do lists</title>',response.content)
self.assertTrue(response.content.endswith(b'</html>'))
运行单元测试(python manage.py test),得到结果:
TypeError: home_page() takes 0 positional arguments but 1 was given
改动代码lists/views.py:
def home_page(request):
pass
运行测试:
self.assertTrue(response.content.startwith(b'<html>'))
AttributeError: 'NoneType' object has no attribute 'content'
继续修改,使用django.http.HttpResponse:
from django.shortcuts import render
from django.http import HttpResponse
# Create your views here.
def home_page(request):
return HttpResponse()
再运行测试:
self.assertTrue(response.content.startswith(b'<html>'))
AssertionError: False is not true
再编写代码:
def home_page(request):
return HttpResponse('<html>')
运行结果:
self.assertIn(b'<title>To-Do lsit</title>',response.content)
AssertionError: b'<title>To-Do lsit</title>' not found in b'<html>'
编写代码:
def home_page(request):
return HttpResponse('<html><title>To-Do lists</title>')
运行测试:
self.assertTrue(response.content.endswith(b'</html>'))
AssertionError: False is not true
再次修改:
def home_page(request):
return HttpResponse('<html><title>To-Do lists</title></html>')
测试结果:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.004s
OK
Destroying test database for alias 'default'...
现在进行功能测试。如果关闭开发服务器,记得启动。
$ python functional_tests.py
F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "functional_tests.py", line 17, in test_can_start_a_list_and_retrieve_it_later
self.fail('Finish the test!')
AssertionError: Finish the test!
----------------------------------------------------------------------
Ran 1 test in 3.717s
FAILED (failures=1)
此时成功编写了一个网页(进入http://localhost:8000/,查看网页源代码,与home_page中传入的html代码相同)。
提交版本信息:
$ git diff #会显示test.py中的新测试方法,以及views.py中的视图
$ git commit -am"Basic view now returns minimal HTML"
$ git log --oneline #查看提交信息进展
c28336e (HEAD -> main) Basic view now returns minimal HTML
cdce47d First unit test url mapping,dummy view
2f0bf75 Add app for lists,with deliberately failing unit test
ba11276 使用注释编写规格的首个功能测试,而且使用了unittest
ee94bbd First commit:First FT and basic Django config
第4章:编写这些测试有什么用
打开functional_tests.py文件,拓展其中的功能测试:
from selenium import webdriver
import unittest
from selenium.webdriver.common.keys import Keys
class NewVisitorTest(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Chrome()
self.browser.implicitly_wait(3)
def tearDown(self):
self.browser.quit()
def test_can_start_a_list_and_retrieve_it_later(self):
self.browser.get("http://localhost:8000")
self.assertIn('To-Do',self.browser.title)
header_text = self.browser.find_element_by_tag_name('h1').text
self.assertIn('To-Do',header_text)
inputbox = self.browser.find_element_by_id('id_new_item')
self.assertEqual(
inputbox.get_attribute('placeholder'),
'Enter a to-do item'
)
inputbox_send_keys('Buy peacock feathers')
inputbox.send_keys(Keys.ENTER)
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertTrue(
any(row.text == '1:Buy peacock feathers' for row in rows)
)
self.fail('Finish the test!')
运行功能测试:
$ python manage.py functional_tests.py
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"h1"}
(Session info: chrome=97.0.4692.71)
提交版本信息:
git diff
git commit -am"Functional test now checks we can input a to-do item"
看一下lists/tests.py中的单元测试。一般来说,单元测试"不测试常量",而以文本形式测试html很大程度上就是测试常量。此外,在python代码中插入原始字符更好的方法是使用模板(把html放在html文件中)。
使用模板重构
重构:在功能不变的前提下改进代码
重构需要先检测测试能否通过:
python manage.py test
- 把html字符串提取出来写入单独的文件:创建目录lists/templates,新建文件home.html,然后把html写入这个文件;
- 修改视图函数:
def home_page(request):
'''
render第一个参数是请求对象,第二个参数是渲染的模板名,Django会自动在所有应用目录中搜索名为templates的文件夹,然后根据模板内容构建一个HttpResponse对象
'''
return render(request,'home.html')
查看模板是否起作用:
ERROR: test_home_page_returns_correct_html (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\study-day-day-up!\练习项目\TDDTest\ch01\superlists\lists\tests.py", line 23, in test_home_page_returns_correct_html
response = home_page(request)
File "D:\study-day-day-up!\练习项目\TDDTest\ch01\superlists\lists\views.py", line 6, in home_page
return render(request,'home.html')
File "D:\Anaconda3\envs\env1\lib\site-packages\django\shortcuts.py", line 36, in render
content = loader.render_to_string(template_name, context, request, using=using)
File "D:\Anaconda3\envs\env1\lib\site-packages\django\template\loader.py", line 61, in render_to_string
template = get_template(template_name, using=using)
File "D:\Anaconda3\envs\env1\lib\site-packages\django\template\loader.py", line 19, in get_template
raise TemplateDoesNotExist(template_name, chain=chain)
django.template.exceptions.TemplateDoesNotExist: home.html
----------------------------------------------------------------------
Ran 2 tests in 0.017s
错误原因:无法找到模板(没有正式在Django中注册lists应用)
解决,打开settings.py文件,把lists加进去:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'lists',
]
此时测试通过。
注意:有的文本编辑器会在文件的最后一行添加一个空行,会导致最后一个断言失败,可以在lists/tests.py中做出修改:
self.assertTrue(response.content.strip().endswith(b'</html>'))
检查是否渲染了正确的模板也可以使用Django中的另一个辅助函数render_to_string:
from django.template.loader import render_to_string
[...]
def test_home_page_returns_correct_html(self):
request = HttpRequest()
response = home_page(request)
expected_html = render_to_string('home.html')
# .decode()把response.content中的字节转换成python中的Unicode字符串
self.assertEqual(response.content.decode(),expected_html)
重构后做一次提交:
git status
git add .
git diff --staged
git commit -m"Refactor home page view to use a template"
此时功能测试还是失败的。修改代码,让它通过。
首先需要一个h1元素(home.html文件):
<html>
<head>
<title>To-Do lists</title>
</head>
<body>
<h1>Your To-Do list</h1>
</body>
</html>
运行功能测试,看是否认同这次修改:
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="id_new_item"]"}
(Session info: chrome=97.0.4692.71)
继续修改:
[...]
<h1>Your To-Do list</h1>
<input id="id_new_item"/>
[...]
现在有:
AssertionError: '' != 'Enter a to-do item'
加上占位文字:
<input id="id_new_item" placeholder="Enter a to-do item"/>
得到结果:
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="id_list_table"]"}
在页面中加入表格:
<table id="id_list_table">
</table>
测试结果:
File "functional_tests.py", line 34, in test_can_start_a_list_and_retrieve_it_later
any(row.text == '1:Buy peacock feathers' for row in rows)
AssertionError: False is not true
错误分析:没有提供明确的失败消息—>自定义错误消息传给assertTrue方法(functional_tests.py):
self.assertTrue(
any(row.text == '1:Buy peacock feathers' for row in rows),"New to-do item did not appear in table"
)
运行结果:
AssertionError: False is not true : New to-do item did not appear in table
做提交:
git diff
git commit -am"Front page HTML now generated from a template"
TDD(Test-Driven Development)总体流程总结:
如果既有功能测试又有单元测试:
—TO BE CONTINUE