【有书共读15】Python测试驱动开发 读书笔记07
编写这些测试有什么用
编程就像从井里打水
编程其实很难,我们的成功往往得益于自己的聪明才智。假如我们不那么聪明,TDD 就能 助我们一臂之力。
测试是一种技能,不是天生就会的。因为很多结果不会立刻显现,需要等待很长一段时 间,所以目前你要强迫自己这么做。
细化测试每个函数的好处
#笔记##读书笔记##测试##Python# 就目前而言,测试简单的函数和常量看起来有点傻。你可能觉得不遵守这么严格的规 则,漏掉一些单元测试,应该也算得上是 TDD。但是在这本书中,我所演示的是完整 而严格的 TDD 流程。
像学习武术中的招式一样,在不受影响的可控环境中才能让技能 变成肌肉记忆。现在看起来之所以琐碎,是因为我们刚开始举的例子很简单。程序变 复杂后问题就来了,到时你就知道测试的重要性了。
你要面临的危险是,复杂性逐渐 靠近,而你可能没发觉,但不久之后你就会变成温水煮青蛙。 我赞成为简单的函数编写细化的简单测试,关于这一观点我还有这么两点要说。
首先,既然测试那么简单,写起来就不会花很长时间。所以,别抱怨了,只管写就 是了。 其次,占位测试很重要。先为简单的函数写好测试,当函数变复杂后,这道心理障碍 就容易迈过去。
你可能会在函数中添加一个 if 语句,几周后再添加一个 for 循环,不 知不觉间就将其变成一个基于元类(meta-class)的多态树状结构解析器了。因为从一开始你就编写了测试,每次修改都会自然而然地添加新测试,最终得到的是一个测试 良好的函数。
相反,如果你试图判断函数什么时候才复杂到需要编写测试的话,那就 太主观了,而且情况会变得更糟,因为没有占位测试,此时开始编写测试需要投入很 多精力,每次改动代码都冒着风险,你开始拖延,很快青蛙就煮熟了。
不要试图找一些不靠谱的主观规则,去判断什么时候应该编写测试,什么时候可以全 身而退。我建议你现在遵守我制定的训练方法,因为所有技能都一样,只有花时间学 会了规则才能打破规则。
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import unittest
class NewVisitorTest(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Firefox()
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')
# 她注意到网页的标题和头部都包含“To-Do”这个词
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'
)
# 她在一个文本框中输入了“Buy peacock feathers(购买孔雀羽毛) ”
# 伊迪丝的爱好是使用假蝇做鱼饵钓鱼
inputbox.send_keys('Buy peacock feathers')
# 她按回车键后,页面更新了
# 待办事项表格中显示了“1: 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)
)
# 页面中又显示了一个文本框,可以输入其他的待办事项
# 她输入了“Use peacock feathers to make a fly(使用孔雀羽毛做假蝇) ”
# 伊迪丝做事很有条理
self.fail('Finish the test!')
# 页面再次更新,她的清单中显示了这两个待办事项
[...]
from selenium.webdriver.common.keys import Keys
import unittest
class NewVisitorTest(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Firefox()
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')
# 她注意到网页的标题和头部都包含“To-Do”这个词
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'
)
# 她在一个文本框中输入了“Buy peacock feathers(购买孔雀羽毛) ”
# 伊迪丝的爱好是使用假蝇做鱼饵钓鱼
inputbox.send_keys('Buy peacock feathers')
# 她按回车键后,页面更新了
# 待办事项表格中显示了“1: 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)
)
# 页面中又显示了一个文本框,可以输入其他的待办事项
# 她输入了“Use peacock feathers to make a fly(使用孔雀羽毛做假蝇) ”
# 伊迪丝做事很有条理
self.fail('Finish the test!')
# 页面再次更新,她的清单中显示了这两个待办事项
[...]
我们使用了 Selenium 提供的几个用来查找网页内容的方法:find_element_by_tag_name,find_element_by_id 和 find_elements_by_tag_name(注意有个 s,也就是说这个方***返回 多个元素)。
还使用了 send_keys,这是 Selenium 在输入框中输入内容的方法。你还会看到使 用了 Keys 类(别忘了导入),它的作用是发送回车键等特殊的按键,还有 Ctrl 等修改键。
小心 Selenium 中 find_element_by... 和 find_elements_by... 这两类函 数的区别。前者返回一个元素,如果找不到就抛出异常;后者返回一个列 表,这个列表可能为空。
还有,留意一下 any 函数,它是 Python 中的原生函数,却鲜为人知。如果你不懂 Python 的话,我告诉你,any 函数的参数是个生成器表达式(generator expression),类似于列表推导(list comprehension),但比它更为出色。
你需要仔细研究这 个概念。能搜到 Guido 对这一概念的精彩解释(http://python-history.blogspot.co.uk/2010/06/ from-list-comprehensions-to-generator.html)。
读完之后你就会知道,这个函数可不仅仅是为 了让编程惬意。 看一下测试进展如何:
$ python3 functional_tests.py
[...]
selenium.common.exceptions.NoSuchElementException: Message: 'Unable to locate element: {"method":"tag name","selector":"h1"}' ; Stacktrace: [...]
解释一下,测试报错在页面中找不到元素。看一下如何在首页的 HTML 中加入这个 元素。 大幅修改功能测试后往往有必要提交一次。初稿中我没这么做,想通之后就后悔了,可是 已经和其他代码混在一起提交了。
其实提交得越频繁越好:
$ git diff # 会显示对functional_tests.py的改动
$ git commit -am "Functional test now checks we can input a to-do item"