使用单元测试测试简单的首页
在上一章结尾,我们有一个简单的测试例子,但是这个例子执行是失败的
失败的原因是浏览器的首页标题不是To-Do,从这章开始编写这个应用
第一个Django应用,第一个单元测试
执行命令创建一个新的工程应用,
$ python manage.py startapp lists
这个命令会在superlists 文件夹中创建子文件夹lists,与superlists 子文件夹相邻,并在lists中创建一些文件,用来保存模型、视图以及目前最关注的测试Case:
superlists/
├─ db.sqlite3
├─ functional_tests.py
├─ lists
│ ├─ admin.py
│ ├─ apps.py
│ ├─ __init__.py
│ ├─ migrations
│ │ └─ __init__.py
│ ├─ models.py
│ ├─ tests.py
│ └─ views.py
├─ manage.py
└─ superlists
├─ __init__.py
├─ __pycache__
├─ settings.py
├─ urls.py
└─ wsgi.py
单元测试与功能测试的区别
作者给出的区别是:
功能测试站在用户的角度从外部测试应用,单元测试则站在程序员的角度从内部测试应用。
如果采用TDD方法进行这两个不同类型的测试,采用的工作流程大致如下
(1) 先写功能测试Case,从用户的角度描述应用的新功能。
(2) 功能测试失败后,再去编写代码让它通过(或者说至少让当前失败的测试通过)。同时,使单元测试Case定义代码需要实现的功能,保证为应用中的每一行代码(至少)编写一个单元测试。
(3) 单元测试失败后,编写最少量的应用代码,刚好让单元测试通过。
(4) 然后,再去执行功能测试Case,看能否通过,或者有没有通过。这会促使我们编写一些新的单元测试和代码等。
从上面这里可以看的出来
功能测试是站在高层驱动开发,而单元测试则是从底层驱动开发
Django 中的单元测试
我们打开刚才新建的lists/tests.py文件
Tests.py是Django库提供的,是在标准版unittest.TestCase的增强版,也添加了一些Django的专用功能,后面会有介绍
我们已经知道了TDD循环从失败的测试开始,再去编写代码让测试通过
我们写一个错误的会失败的测试,打开lists/test.py
from django.test import TestCase
class SmokeTest(TestCase):
def test_bad_maths(self):
# 断言是否正确,1+1 !=3,所以这个Case是失败的
self.assertEqual(1+1, 3)
在superlists目录运行 python manage.py test
,执行结果如下:
执行结果:失败,先放一放,后面再解决
执行结果虽然是失败的,但是单元测试是可以运行的,我们先提交代码,
$ git status # 会显示一个消息,说没跟踪lists/
$ git add lists # 这里的lists,第二次创建时,发现创建的是list,需要注意
$ git diff --staged # 显示将要提交的内容差异
$ git commit -m "Add app for lists, with deliberately failing unit test" # -m 的作用是让你在命令行中直接编写提交备注
Django中的MVC、URL和视图函数
Django 遵守了经典的模型- 视图- 控制器(Model-View-Controller,MVC)模式,但并没严格遵守Django有模型,视图其实像是控制器,模板其实才是视图。 Django 和服务器Web框架一样,主要任务是决定用户访问网站中的某个 URL 时做些什么
Django的工作流程有点儿类似下述过程。
(1) 针对某个URL 的HTTP 请求进入
(2) Django使用一些规则决定由哪个视图函数处理这个请求(这一步叫作解析URL)。
(3) 选中的视图函数处理请求,然后返回HTTP 响应
因此要测试两件事。
解析网站根路径(“/”)的URL,将其对应到我们编写的某个视图函数上?
让视图函数返回一些HTML,让功能测试通过?
现在我们打开 lists/tests.py,把之前错误的代码修改如下
from django.urls import resolve
from django.test import TestCase
from lists.views import home_page # 2
class HomePageTest(TestCase):
def test_root_url_resolves_to_home_page_view(self):
found = resolve('/') # 1
self.assertEqual(found.func, home_page) # 1
➊ resolve 是Django 内部使用的函数,用于解析URL,并将其映射到相应的视图函数上。检查解析网站根路径“/”时,是否能找到名为home_page 的函数。
➋ 这个函数是什么?这是接下来要定义的视图函数,其作用是返回所需的HTML。从import 语句可以看出,要把这个函数保存在文件lists/views.py 中。
现在我们运行修改后的代码
$ python manage.py test
运行提示,无法找到“home_page”
,这是因为我们导入了没有定义的函数,这个错误也是预期的,从TDD的思想看,功能测试和单元测试都失败了,我们就可以写应用代码了,让这些测试通过
编写应用代码
使用TDD 时要耐着性子,步步为营。尤其是学习和起步阶段,一次只能修改(或添加)一行代码。每一次修改的代码要尽量少,让失败的测试通过即可
失败的原因是没有找到要导入的home_page,我们现在修正这个问题
打开lists/views.py编写代码:
from django.shortcuts import render
home_page = None
然后再执行python manage.py test
运行依然失败,查看错误日志发现问题是resolve()
方法在解析’/‘时,Django报错404,也就是说Django 无法解析’/'URL的映射
urls.py
Django 用urls.py 文件把URL 映射到视图函数上。在文件夹superlists/superlists 中有个主urls.py文件,这个文件应用于整个网站。
url 条目的前半部分是正则表达式,定义适用于哪些URL。后半部分说明把请求发往何处:发给导入的视图函数,或是别处的urls.py 文件。
现在修改superlists/urls.py文件,文件里有使用说明,大家可以看一看,正则表达式是 ^$
,表示空字符串,同时去掉 admin URL,因为暂时用不到 Django 的管理后台
from django.conf.urls import url
from list import views
urlpatterns = [
url(r'^$', views.home_page, name="home"),
]
结果依然报错,现在不显示404了,视图必须用可调用 列表/元组 在测试头文件里
view must be a callable or a list/tuple in the case of include()
单元测试把地址“/”和文件lists/views.py 中的home_page = None 连接起来了,home_page 无法调用。由此我们知道,要调整一下,把home_page 从None 变成真正的函数。
记住,每次改动代码都由测试驱使
我们继续修改lists /views.py,保存退出
from django.shortcuts import render
def home_page():
pass
然后再运行python manage.py test
执行通过,第一条测试用例通过
至此,通过TDD思想,我们先后编写了功能/单元的测试Case,然后再通过测试Case去编写代码,其中使用了DjangoWeb应用框架,Unittest测试框架。
通过后,我们开始继续提交
$ git status
$ git diff # 会显示urls.py、tests.py和views.py中的变动
$ git commit -am "First unit test and url mapping, dummy view"
为视图编写单元测试
下面我们为视图编写测试,我们不能调用什么都不能做的函数,而是自己定义一个函数,向浏览器返回HTML响应
打开lists/tests.py,添加新的测试Case
from django.urls import resolve
from django.test import TestCase
from django.http import HttpRequest
from lists.views import home_page
class HomePageTest(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)
html = response.content.decode('utf8')
self.assertTrue(html.startswith('<html>'))
self.assertIn('<title>To-Do lists</title>', html)
self.assertTrue(html.endswith('</html>'))
➊ 创建了一个HttpRequest 对象,用户在浏览器中请求网页时,Django 看到的就是 HttpRequest 对象。
➋ 把这个 HttpRequest 对象传给 home_page 视图,得到响应。响应对象是 HttpResponse 类的实例时,你应该不会觉得奇怪。
➌ 然后,提取响应的 .content。得到的结果是原始字节,即发给用户浏览器的 0 和 1。随 后,调用.decode(),把原始字节转换成发给用户的 HTML 字符串。
➍ 断言响应以 标签开头,并在结尾处关闭该标签。
➎ 断言响应中有一个标签,其内容包含单词“To-Do lists”——因为在功能测 试中做了这项测试
执行报错,并提示,home_page(),需要传入参数,错误先放一放,待会修改
TypeError: home_page() takes 0 positional arguments but 1 was given
”单元测试/编写代码”循环
现在开始适应TDD 中的单元测试/ 编写代码循环了
(1) 在终端里运行单元测试,看它们是如何失败的。
(2) 在编辑器中改动最少量的代码,让当前失败的测试通过。
下面我们一次只修改一点的代码,然后运行测试,观察测试结果
打开并编辑lists/views.py,这个用例是定义home_page请求
def home_page(request):
pass
保存后, superlists目录下执行 python manage.py test
,报错:”NoneType对象“没有”conten“属性
继续编辑lists/views,修改代码。执行 python manage.py test
报错:self.assertTrue(html.startswith(’’))结果为假,不为真
from django.http import HttpResponse
def home_page(request):
return HttpResponse()
继续编辑lists/views,修改代码,响应添加 '<html>'
。执行 python manage.py test
报错:AssertionError: ‘
from django.http import HttpResponse
def home_page(request):
return HttpResponse('<html>')
继续编辑lists/views,修改代码, 响应添加<html><title>To-Do lists</title>
。执行 python manage.py test
报错:AssertionError: False is not true
from django.http import HttpResponse
def home_page(request):
return HttpResponse('<html><title>To-Do lists</title>')
继续编辑lists/views,修改代码, 响应添加<html><title>To-Do lists</title></html>
。执行 python manage.py test
报错:AssertionError: False is not true
from django.http import HttpResponse
def home_page(request):
return HttpResponse('<html><title>To-Do lists</title></html>')
执行通过,并返回OK
执行通过,我们根据用户story设计测试Case,然后一遍执行Case,一遍修改应用代码,那里错误就修改那里
直到测试用例完全通过,应用代码完成
单元测试通过后,我们进行功能测试,运行python functional_tests.py
,功能测试执行通过,断言报错:AssertionError: Finish the test!
这个断言报错,注释掉那行代码就可以了,作者这里是故意这样写的,先保留
现在我们查看功能测试代码,测试用例完成了两个功能
第一个是用Selenium get()方法,调用浏览器,请求本地网址
第二个是打开新的网址,这个网址标题页名叫”To-Do“
做完之后,我们开始编写单元测试用例,根据单元测试,利用Django框架来编写应用代码
from selenium import webdriver
import unittest
class NewVisitorTest(unittest.TestCase):
# 执行之前执行
def setUp(self):
self.brower = webdriver.Firefox()
# 执行完之后执行
def tearDown(self):
self.brower.quit()
# 测试用例
def test_can_start_a_list_and_retrieve_it_later(self):
# 伊迪丝听说有一个很酷的在线待办事项应用
# 她去看了这个应用的首页
self.brower.get('http://localhost:8000')
# 她注意到网页的标题和头部都包含“To-Do”这个词
self.assertIn('To-Do', self.brower.title)
# self.fail('Finish the test!')
# 应用邀请她输入一个待办事项
if __name__ == '__main__':
unittest.main(warnings='ignore')
当打开网页时,可以看到网页上的”To-Do lists“。至此,我们完成了一个用户story
然后查看状态,并且进行提交,提交完成后,我们可以用git log --oneline
命令查看提交的历史记录和备注
$ git diff # 会显示tests.py中的新测试方法,以及views.py中的视图
$ git commit -am "Basic view now returns minimal HTML"
$ git log --oneline # 回顾提交历史备注
本章介绍了以下知识
• 新建Django 应用
• Django 的单元测试运行程序
• 功能测试和单元测试之间的区别
• Django 解析URL 的方法,urls.py 文件的作用
• Django 的视图函数,请求和响应对象
• 如何返回简单的HTML