文章目录

  • 第一部分 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)总体流程总结:

python编写驱动程序 python开发驱动_git


如果既有功能测试又有单元测试:

python编写驱动程序 python开发驱动_git_02


—TO BE CONTINUE