Python 测试驱动开发(四)测试及重构的目的(上)

使用Selenium测试用户交互

如果用绳子从井 里提一桶水,井不太深,而且桶不是很满,提起来很容易。就算提满满一桶水,刚开始也很容易。但用不了多久你就累了。TDD 理念好比是一个棘轮,你可以使用它保存当前的进度,休息一会儿,而且能保证进度绝不倒退。

上一章,我们进行到哪里了,忘记了,可以执行命令来看看
注意:从本章开始,我的目录从lists改为list,因为之前的环境不小心删掉了,从新搞了遍环境

进入目录运行 functional_test.py

$ python functional_tests.py

如果这里报错,提示说加载页面出错或者无法连接。
这是因为运行测试之前没有使用manage.py runserver 启动开发服务器。
启动服务

python manage.py runserver

TDD 的优点之一是,永远不会忘记接下该做什么——重新运行测试就知道要接下来要做的事情。

在这里插入图片描述
断言报错返回,Finish the test! 。
我们打开并编辑functional_tests.py 文件,完成功能测试用例

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time
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)
        header_test = self.brower.find_element_by_tag_name('h1').text  # 1
        self.assertIn('To-Do', header_test)

        # 应用邀请她输入一个待办事项
        inputbox = self.brower.find_element_by_id('id_new_item')  # 1
        self.assertEqual(
            inputbox.get_attribute('placeholder'),
            'Enter a to-do item'
        )

        # 她在一个文本框中输入了“Buy peacock feathers”(购买孔雀羽毛)
        # 伊迪丝的爱好是使用假蝇做鱼饵钓鱼
        inputbox.send_keys('Buy peacock feathers')  # 2

        # 她按回车键后,页面更新了
        # 待办事项表格中显示了“1: Buy peacock feathers”
        inputbox.send_keys(Keys.ENTER)  # 3
        time.sleep(1)  # 4
        table = self.brower.find_element_by_id('id_list_table')
        rows = table.find_element_by_tag_name('tr')  # 1
        self.assertTrue(
            any(row.text == '1: Buy peacock feathers' for row in rows)
        )
        
        # 页面中又显示了一个文本框,可以输入其他的待办事项
        # 她输入了“Use peacock feathers to make a fly”(使用孔雀羽毛做假蝇)
        # 伊迪丝做事很有条理
        # 页面再次更新,她的清单中显示了这两个待办事项
        # 伊迪丝想知道这个网站是否会记住她的清单
        # 她看到网站为她生成了一个唯一的URL
        self.fail('Finish the test!')
        
        # 页面再次更新,她的清单中显示了这两个待办事项
        # 伊迪丝想知道这个网站是否会记住她的清单
        # 她看到网站为她生成了一个唯一的URL

if __name__ == '__main__':
    unittest.main(warnings='ignore')

注释里用到的方法如下

  1. 使用了Selenium提供的几个方法用来查找网页的内容:
    find_element_by_tag_name
    find_elements_by_tag_name
    注意第二个元素多了一个s会返回多个元素,也可能返回一个空的列表

  2. 使用了Selenium提供的send_keys方法用来在输入框输入内容

  3. Keys这个类(需要导入) 的作用是让我们发送回车键的按键

  4. 按下回车键会刷新。time.sleep(),表示暂停几秒钟

  5. any函数,这是Python里的原生函数

    any() 函数用于判断给定的可迭代参数 iterable 是否全部为 False,则返回 False,如果有一个为 True,则返回 True。

继续运行python functional_tests.py,报错:Message: Unable to locate element: h1。找不到h1元素
在这里插入图片描述
在对Web应用修改之前,我们进行一次提交

$ git diff # 会显示对functional_tests.py的改动
$ git commit -am "Functional test now checks we can input a to-do item"

遵守“不测试常量”规则,使用模板解决这个问题

我们去看下list/tests.py中的单元测试。测试时要查找特定的HTML字符串,但是这个方法效率很低。所以,单元测试的规则之一是不测试常量,我们以文本形式测试HTML在很大程度上就是测试常量
如果有以下的代码:

   wibble = 3

在测试时,就没有必要按照以下方法写

from myprogram import wibble
assert wibble == 3

单元测试的本质是逻辑、流程控制和配置。我们编写断言测试HTML字符串中是否有指定的字符串,这个不是单元测试应该做的

在Python代码中插入原始字符串需要是处理HTML错误的。我们有更好的方法,使用模板。如果把HTML放在一个扩展名为.html文件中。Python领域有很多模板框架,Django也有自己的模板系统,而且很好用,我们来看看

使用模板重构

让视图函数返回完全一样的HTML,但是使用不同的处理方式。这个过程叫作重构,在功能不变的前提下改进代码
重构的原则是不能没有测试。我们在做测试驱动开发,测试已经有了,现在只需要检查下测试是否通过,测试通过后才能保证重构前后的表现一致
先把HTML 字符串提取出来写入单独的文件。创建模板的文件夹list/templates并新建文件list/templates/home.html,再把HTML 写入这个文件
这里我用的notepad++有高亮语法

<html>
	<title>To-Do list</title>
</html>

在这里插入图片描述
接下来,我们修改视图,让它去调用home.html:

from django.shortcuts import render

def home_page(request):
    return render(request,'home.html')

在这里插入图片描述
现在我们不用自己构建HttpResponse对象,可以使用Django中的render函数。

render

第一个参数是请求对象(后面介绍)
第二个参数是渲染的模板名
Django会自动在所有的应用目录中搜索名为templates的文件夹,然后根据模板中的内容构建一个HttpResponse 对象

模板是Django 中一个很强大的功能,使用模板的主要优势之一是能把Python 变量代入HTML 文本
这也是为什么使用render 和render_to_string(稍后用到),而不用原生的open 函数手动从硬盘中读取模板文件的原因

现在我们测试下模板,看下模板是否生效

$ python manage.py test

在这里插入图片描述
运行报错:TemplateDoesNotExist: home.html

我们刚才有写home.html模板并且放在了list/templates文件夹中。找不到的原因是没有在Django中注册list应用,需要用startapp 命令添加应用
执行startapp 命令后,还需要配置应用到文件settings.py 中,打开settings.py,找到变量INSTALLED_APPS,把list 目录加进去:

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'list',	# 添加
]

然后再运行测试

$ python manage.py test

在这里插入图片描述
测试通过,至此我们对代码做的小重构通过了测试,从现在开始我们不需要测试常量,只需要检查模板渲染是否正确

Django测试客户端

测试模板渲染是否正确,一种方法是在测试中手动渲染模板,然后与视图返回的结果做比较
在这里我们利用Django 提供的render_to_string 函数
打开并编辑单元测试文件list/tests.py

from django.template.loader import render_to_string
[...]
def test_home_page_returns_correct_html(self):
request = HttpRequest()
response = home_page(request)
html = response.content.decode('utf8')
expected_html = render_to_string('home.html')
self.assertEqual(html, expected_html)

运行测试,通过

$ python manage.py test

在这里插入图片描述
这样测试有点麻烦,测试时调用.decode() 和.strip()又测试太麻烦,所以选择使用Django自带的测试客户端(TestClient)检查使用那个模板的原生方式。
我们从新修改list/tests.py,来看看效果

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):
        response = self.client.get('1')     # 1

        html = response.content.decode('utf8')  # 2
        self.assertTrue(html.startswith('<html>'))
        self.assertIn('<title>To-Do list</title>', html)
        self.assertTrue(html.strip().endswith('</html>'))
        self.assertTemplateUsed(response, 'home.html')  # 3
  1. 不用手动创建HttpRequest 对象,也不用直接调用视图函数,而是调用self.client.get方法直接传入要测试的URL
  2. 暂时保留
  3. .assertTemplateUsed 是Django TestCase 类提供的测试方法,用于检查响应是使用哪个模板渲染(注意,这个方法只能测试通过测试客户端获取的响应)

运行测试

$ python manage.py test

在这里插入图片描述
测试通过后,我们对我们的测试结果有疑虑,所以我们需要搞点小事情
打开并编辑单元测试文件list/tests.py

self.assertTemplateUsed(response, 'home.html')

修改为

self.assertTemplateUsed(response, 'wrong.html')

运行测试

$ python manage.py test

在这里插入图片描述
我们再将断言修改回去,并且对测试Case进行了精简,顺便把原来的 test_root_url_resolves 测试Case删除,因为Django的测试客户端已经后台测试过来了,我们将2个麻烦的测试Case精简成一个

from django.test import TestCase


class HomePageTest(TestCase):
    def test_uses_home_template(self):
        response = self.client.get("/")
        self.assertTemplateUsed(response, "home.html")

运行测试

$ python manage.py test

在这里插入图片描述

关于重构

这个重构的例子很烦琐。但正如Kent Beck 在Test-Driven Development: By Example 一书中所说的:“我是推荐你在实际工作中这么做吗?不是。我只是建议你要知道怎么按照这种 方式做。”
其实,写这一部分时我的第一反应是先修改代码,直接使用assertTemplateUsed 函数,删除那三个多余的断言,只在渲染得到的结果中检查期望看到的内容,然后再修改代码。
但要注意,如果真这么做了可能就会犯错,因为我可能不会在模板中编写正确的 和 标签,而是随便写一些字符串。

重构后,我们做一次提交

$ git status # 会看到tests.py、views.py、settings.py以及新建的templates文件夹
$ git add . # 还会添加尚未跟踪的templates文件夹
$ git diff --staged # 审查我们想提交的内容
$ git commit -m "Refactor home page view to use a template"

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/sevensolo/article/details/96611992
今日推荐