Python 测试驱动开发读书笔记(三)使用单元测试测试简单的首页

使用单元测试测试简单的首页

在上一章结尾,我们有一个简单的测试例子,但是这个例子执行是失败的
在这里插入图片描述
失败的原因是浏览器的首页标题不是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: ‘To-Do lists’ not found in ‘’

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!
a
这个断言报错,注释掉那行代码就可以了,作者这里是故意这样写的,先保留
在这里插入图片描述
现在我们查看功能测试代码,测试用例完成了两个功能
第一个是用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

猜你喜欢

转载自blog.csdn.net/sevensolo/article/details/94293303