unittest 是 python 的单元测试框架,主要有以下作用:
unittest 中有四个很重要的概念: test fixture、test case、test suite、test runner
test fixture
对一个测试用例环境的搭建和销毁,就是一个 fixture,通过重写 setUp() 方法和 tearDown() 方法来实现
setUp() 方法可以进行测试环境的搭建,比如获取浏览器的驱动、设置测试 URL、连接数据库等操作
tearDown() 方法及逆行环境的销毁,可以关闭浏览器、关闭数据库等
test case
一个 test case 就是一个测试用例,即一个完整的测试流程,包括 setUp 方法、tearDown 方法以及完成测试过程的代码
test suite
测试套件,test suite 用来将多个测试用例组装在一起
test runner
在 unittest 框架中,通过 textTestRunner 类下的 run() 方法来执行测试用例或者测试套件
下面是一个使用了 unittest 框架的简单测试脚本
from selenium import webdriver
import time
import unittest
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
class bing(unittest.TestCase):
def setUp(self):
print("------setUp------")
self.driver = webdriver.Chrome()
self.url = "https://cn.bing.com/"
self.driver.maximize_window()
time.sleep(3)
def tearDown(self):
print("------tearDown------")
self.driver.quit()
def test_search(self):
driver = self.driver
url = self.url
driver.get(url)
time.sleep(3)
driver.find_element_by_id("sb_form_q").send_keys("python")
time.sleep(3)
driver.find_element_by_id("sb_form_q").send_keys(Keys.ENTER)
time.sleep(3)
def test_closeImg(self):
driver = self.driver
url = self.url
driver.get(url)
time.sleep(3)
driver.find_element_by_id("id_sc").click()
time.sleep(3)
a = driver.find_element_by_xpath("//*[@id='qs_iotd_ctrl']/div/div[3]/div")
ActionChains(driver).move_to_element(a).perform()
time.sleep(3)
a.click()
time.sleep(3)
if __name__ == "__main__":
unittest.main()
将多个测试用例组织起来形成一个 test suite 测试套件,就可以一次性执行多个测试用例
假设有如下两个测试用例:
testBing.py
from selenium import webdriver
import time
import unittest
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
class bing(unittest.TestCase):
def setUp(self):
print("------setUp------")
self.driver = webdriver.Chrome()
self.url = "https://cn.bing.com/"
self.driver.maximize_window()
time.sleep(3)
def tearDown(self):
print("------tearDown------")
self.driver.quit()
def test_search(self):
driver = self.driver
url = self.url
driver.get(url)
time.sleep(3)
driver.find_element_by_id("sb_form_q").send_keys("python")
time.sleep(3)
driver.find_element_by_id("sb_form_q").send_keys(Keys.ENTER)
time.sleep(3)
def test_closeImg(self):
driver = self.driver
url = self.url
driver.get(url)
time.sleep(3)
driver.find_element_by_id("id_sc").click()
time.sleep(3)
a = driver.find_element_by_xpath("//*[@id='qs_iotd_ctrl']/div/div[3]/div")
ActionChains(driver).move_to_element(a).perform()
time.sleep(3)
a.click()
time.sleep(3)
if __name__ == "__main__":
unittest.main()
testBaidu.py
from selenium import webdriver
import time
import unittest
from selenium.webdriver.common.action_chains import ActionChains
class baidu(unittest.TestCase):
def setUp(self):
print("------setUp------")
self.driver = webdriver.Chrome()
self.url = "https://www.baidu.com/"
self.driver.maximize_window()
time.sleep(3)
def tearDown(self):
print("------tearDown------")
self.driver.quit()
def test_search(self):
url = self.url
driver = self.driver
driver.get(url)
time.sleep(3)
driver.find_element_by_id("kw").send_keys("python")
time.sleep(3)
driver.find_element_by_id("su").click()
time.sleep(3)
def test_hao_search(self):
url = self.url
driver = self.driver
driver.get(url)
time.sleep(3)
driver.find_element_by_link_text("hao123").click()
time.sleep(3)
def test_baiduTranslation(self):
url = self.url
driver = self.driver
driver.get(url)
time.sleep(3)
a = driver.find_element_by_link_text("更多")
ActionChains(driver).move_to_element(a).perform()
time.sleep(3)
driver.find_element_by_xpath("//*[@id='s-top-more']/div[1]/a[1]").click()
time.sleep(3)
if __name__ == '__main__':
unittest.main()
unittest 中提供了多种方法来构建测试套件
TestSuite 类的 addTest 方法可以把不同的测试类中的测试方法组装带测试套件中,但是 addTest 方法一次只能把一个类中的一个方法添加到测试套件中
import unittest
from src_selenium.src_unittest import testBing
from src_selenium.src_unittest import testBaidu
def createSuite():
# addTest
suite = unittest.TestSuite()
suite.addTest(testBing.bing("test_closeImg"))
suite.addTest(testBing.bing("test_search"))
suite.addTest(testBaidu.baidu("test_search"))
suite.addTest(testBaidu.baidu("test_hao_search"))
return suite
# 执行测试套件
if __name__ == '__main__':
suite = createSuite()
# verbosity 设置日志级别,2最高,0最低
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
makSuite 方法配合 addTest 方法可以实现一次将某个测试类中的所有测试方法添加到测试套件
只需要在 makeSuite 方法中传入测试类名即可
import unittest
from src_selenium.src_unittest import testBing
from src_selenium.src_unittest import testBaidu
def createSuite():
# makeSuite
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(testBing.bing))
suite.addTest(unittest.makeSuite(testBaidu.baidu))
return suite
# 执行测试套件
if __name__ == '__main__':
suite = createSuite()
# verbosity 设置日志级别,2最高,0最低
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
TestLoader 方法的作用与 makeSuite 方法一样,都是将某个测试类中的所有测试方法添加到测试套件中
不同的是 TestLoader 不需要配合 addTest 使用,直接使用 unittest.TestLoader().loadTestsFromTestCase() 方法即可,
import unittest
from src_selenium.src_unittest import testBing
from src_selenium.src_unittest import testBaidu
def createSuite():
# TestLoader
suite1 = unittest.TestLoader().loadTestsFromTestCase(testBing.bing)
suite2 = unittest.TestLoader().loadTestsFromTestCase(testBaidu.baidu)
suite = unittest.TestSuite([suite1, suite2])
return suite
# 执行测试套件
if __name__ == '__main__':
suite = createSuite()
# verbosity 设置日志级别,2最高,0最低
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
discover() 的应用
discover 方法可以将某个目录下的所有符合标准的脚本文件中的所有测试方法添加到测试套件中
使用 unittest.defaultTestLoader.discover() 方法,第一个参数填入某个目录的绝对路径,第二个参数填入标准文件名,testB*.py 则表示以 testB 开头的 python 文件,第三个参数表示测试模块的顶层目录,一版设置为 None 即可
import unittest
from src_selenium.src_unittest import testBing
from src_selenium.src_unittest import testBaidu
def createSuite():
# discover 的应用
discover = unittest.defaultTestLoader.discover("D:\\JAVA\\Python\\project\\src_selenium\\src_unittest", pattern="testB*.py", top_level_dir=None)
print(discover)
return discover
# 执行测试套件
if __name__ == '__main__':
suite = createSuite()
# verbosity 设置日志级别,2最高,0最低
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
unittest 框架在执行多个测试方法时,会根据类名或者方法名的 ASCII 码顺序进行执行,即 0 ~ 9,A ~ Z,a ~ z
就测试方法来说,顺序是根据 “ test_ ” 后的单词进行排序
如果在某一次测试中不需要执行某一个测试方法,就需要把这个测试方法忽略不执行,使用 @unittest.skip() 注解就可以完成,写入的参数会在控制台打印出
@unittest.skip("test_hao_search 被忽略")
def test_hao_search(self):
url = self.url
driver = self.driver
driver.get(url)
time.sleep(3)
driver.find_element_by_link_text("hao123").click()
time.sleep(3)
对于每一个单独的测试用例来说,必然会有预期结果和实际结果,通过比对预期结果和实际结果就可以判断测试用例是否通过
反映到代码中就是断言,断言通过则会继续执行下面的代码,否则对应的测试方法就会停止或者生成错误信息,但不会影响其他测试方法的执行
unittest 中提供了丰富的断言方法
msg 参数为断言未通过时的提示语,可以自定义,也可以不写此参数
断言方法 | 描述 |
---|---|
assertEqual(arg1, arg2, msg=None) | 验证 “arg1== arg2” 是否通过 |
assertNotEqual(arg1, arg2, msg=None) | 验证 “arg1!= arg2” 是否通过 |
assertTrue(arg, msg=None) | 验证 “args 为 True” 是否通过 |
assertFalse(arg, msg=None) | 验证 “args 为 False” 是否通过 |
assertIs(arg1, arg2, msg=None) | 验证 “arg1 和 arg2 是同一个对象” 是否通过 |
assertNotIs(arg1, arg2, msg=None) | 验证 “arg1 和 arg2 不是同一个对象”是否通过 |
assertIsNone(arg, msg=None) | 验证 “arg 是 None” 是否通过 |
assertIsNotNone(arg, msg=None) | 验证 “arg 不是 None” 是否通过 |
assertIn(arg1, arg2, msg=None) | 验证 “arg1 是 arg2 的子串” 是否通过 |
assertNotIn(arg1, arg2, msg=None) | 验证 “arg1 不是 arg2 的子串” 是否通过 |
assertIsInstance(obj, cls, msg=None) | 验证 “obj 是 cls 的实例” 是否通过 |
assertNotIsInstance(obj, cls, msg=None) | 验证 “obj 不是 cls 的实例” 是否通过 |
断言用法小示例:
def test_search(self):
driver = self.driver
url = self.url
driver.get(url)
time.sleep(3)
driver.find_element_by_id("sb_form_q").send_keys("python")
time.sleep(3)
driver.find_element_by_id("sb_form_q").send_keys(Keys.ENTER)
time.sleep(3)
print(driver.title)
# 验证 “打开的网页 title 不是 ‘python - 搜索’” 是否通过
self.assertNotEqual("python - 搜索", driver.title, msg="未打开页面")
未通过就会在控制台给出错误信息
脚本执行完成之后,还需要生成一个测试报告,这里就可以使用 HTMLTestRunner.py 来生成 HTML 形式的测试报告
HTMLTestRunner.py 文件,下载地址: http://tungwaiyip.info/software/HTMLTestRunner.html
下载后将其放入 python 安装目录的 Lib 目录下
HTMLTestRunner 支持python2.7。python3可以参见http://blog.51cto.com/hzqldjb/1590802来进行修改。
import HTMLTestRunner
import os.path
import sys
import time
import unittest
from src_selenium.src_unittest import testBing
from src_selenium.src_unittest import testBaidu
def createSuite():
# makeSuite
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(testBing.bing))
suite.addTest(unittest.makeSuite(testBaidu.baidu))
return suite
if __name__ == '__main__':
# 创建一个存放 HTML 报告的文件夹
curPath = sys.path[0] # 获取当前文件的路径
if not os.path.exists(curPath + "/result"):
# 判断是否存在此文件夹,不存在则创建一个
os.makedirs(curPath + "/result")
# 用当前时间作为 HTML 报告的文件名
now = time.strftime("%Y-%m-%d-%H%M%S", time.localtime(time.time())) # 获取当前时间
# 拼接报告地址
fileName = curPath + "/result/" + now + "report.html"
# 出报告
with open(fileName, "wb") as f:
runner = HTMLTestRunner.HTMLTestRunner(f, title="测试报告", description="用例执行情况", verbosity=2)
suite = createSuite()
runner.run(suite)
在测试套件的主函数中添加上述代码,就可以生成一个 HTML 报告并保存到指定位置
报告打开之后如下所示:
标红的就是未通过的用例,点击 fail 还会显示详细的错误信息
在用例执行时如果可以将错误现场自动截图,那么就会给我们定位错误带来方便
编写一个函数,函数的主要功能就是截图并且保存到指定位置,此函数不以 “test_” 开头,即不让 unittest 自动执行该函数,将异常捕获之后,只在需 expect 中调用即可
使用 webdriver 下的 get_screenshot_as_file() 方法进行截图并保存
def test_search(self):
driver = self.driver
url = self.url
driver.get(url)
time.sleep(3)
driver.find_element_by_id("sb_form_q").send_keys("python")
time.sleep(3)
driver.find_element_by_id("sb_form_q").send_keys(Keys.ENTER)
time.sleep(3)
try:
self.assertNotEqual("python - 搜索", driver.title)
except:
self.saveScreenshot(driver, "search.png")
def saveScreenshot(self, driver, filename):
# 创建文件夹保存截图
if not os.path.exists("./img"):
os.makedirs("./img")
# 使用时间作为文件名
now = time.strftime("%Y-%m-%d-%H%M%S", time.localtime(time.time()))
driver.get_screenshot_as_file("./img/" + now + "-" + filename)
time.sleep(1)
异常被捕获之后就不会在控制台显示了,我们处理异常的方式是截图错误现场并保存起来
前面我们所有的数据和用例都是写在一起的,但是如果想在同一个用例中测试多个不同数据,按照之前的方法就需要编写多个用例,但其实可以使用 ddt 数据驱动来完成,ddt 可以使一个用例测试多个数据
unittest 没有自带的数据驱动,所以我们需要使用 pip 另外下载 ddt 数据驱动
ddt 中常用的注解:
一个参数的多个不同值
使用 @ddt 参数修饰测试类,@data 注解修饰测试方法并传入参数的不同值,在方法的参数列表中添加一个参数作为本方法的测试参数
from ddt import ddt, data, unpack, file_data
from selenium import webdriver
import time
import unittest
from selenium.webdriver.common.keys import Keys
@ddt
class bing(unittest.TestCase):
def setUp(self):
print("------setUp------")
self.driver = webdriver.Chrome()
self.url = "https://cn.bing.com/"
self.driver.maximize_window()
time.sleep(3)
def tearDown(self):
print("------tearDown------")
self.driver.quit()
# 分别测试 bing 搜索 python、java、rust
@data("python", "java", "rust")
def test_search(self, value):
driver = self.driver
url = self.url
driver.get(url)
time.sleep(3)
driver.find_element_by_id("sb_form_q").send_keys(value)
time.sleep(3)
driver.find_element_by_id("sb_form_q").send_keys(Keys.ENTER)
time.sleep(3)
if __name__ == "__main__":
unittest.main()
多个参数的多个不同值
修饰的注解不变,变得只是 @data 注解中的数据,可以使用列表表示一组测试数据
需要注意的是:当有多个参数时,需要使用 @unpack 注解修饰测试方法以映射多个参数
如下形式表示两个参数的多组数据
@data([3, 2], [4, 3], [5, 3])
from selenium import webdriver
import time
import unittest
from ddt import data, ddt, file_data, unpack
@ddt
class baidu(unittest.TestCase):
def setUp(self):
print("------setUp------")
self.driver = webdriver.Chrome()
self.url = "https://www.baidu.com/"
self.driver.maximize_window()
time.sleep(3)
def tearDown(self):
print("------tearDown------")
self.driver.quit()
@data(["python", "python_百度搜索"], ["java", "python_百度搜索"], ["rust", "python_百度搜索"])
@unpack
def test_search(self, value, expect_value):
url = self.url
driver = self.driver
driver.get(url)
time.sleep(3)
driver.find_element_by_id("kw").send_keys(value)
time.sleep(3)
driver.find_element_by_id("su").click()
time.sleep(3)
self.assertEqual(expect_value, driver.title, msg="网页未打开")
if __name__ == '__main__':
unittest.main()
同样使用使用 @data() 注解修饰测试方法,但是参数填入的是解析 txt/csv 文件的方法
需要注意的是:文件的开头一行必须是 data,后面每一行为一组数据,这是固定格式,如下所示
import csv
from selenium import webdriver
import time
import unittest
from selenium.webdriver.common.action_chains import ActionChains
from ddt import data, ddt, file_data, unpack
def get_txt(file_name):
tmp_data = []
with open("./data/" + file_name, "r") as f:
readers = csv.reader(f, delimiter=",", quotechar="|")
next(readers, None)
for row in readers:
rows = []
for i in row:
rows.append(i)
tmp_data.append(rows)
print(tmp_data)
return tmp_data
@ddt
class baidu(unittest.TestCase):
def setUp(self):
print("------setUp------")
self.driver = webdriver.Chrome()
self.url = "https://www.baidu.com/"
self.driver.maximize_window()
time.sleep(3)
def tearDown(self):
print("------tearDown------")
self.driver.quit()
@data(*get_txt("test_Baidu.txt"))
@unpack
def test_search(self, value, expect_value):
url = self.url
driver = self.driver
driver.get(url)
time.sleep(3)
driver.find_element_by_id("kw").send_keys(value)
time.sleep(3)
driver.find_element_by_id("su").click()
time.sleep(3)
self.assertEqual(expect_value, driver.title, msg="网页未打开")
if __name__ == '__main__':
unittest.main(verbosity=2)
使用 @file_data(json 文件名) 修饰测试方法即可调用 json 文件中的数据
import os.path
from ddt import ddt, data, unpack, file_data
from selenium import webdriver
import time
import unittest
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
@ddt
class bing(unittest.TestCase):
def setUp(self):
print("------setUp------")
self.driver = webdriver.Chrome()
self.url = "https://cn.bing.com/"
self.driver.maximize_window()
time.sleep(3)
def tearDown(self):
print("------tearDown------")
self.driver.quit()
# @data("python", "java", "rust")
@file_data("./data/test_Bing.json")
def test_search(self, value):
driver = self.driver
url = self.url
driver.get(url)
time.sleep(3)
driver.find_element_by_id("sb_form_q").send_keys(value)
time.sleep(3)
driver.find_element_by_id("sb_form_q").send_keys(Keys.ENTER)
time.sleep(3)
if __name__ == "__main__":
unittest.main()