1. unittest 框架解析
unittest 是 python 的单元测试框架,主要有以下作用:
提供测试用例的组织和执行: unittest 框架可以将成百上千条测试用例组织在一起进行执行
提供丰富的比较方法: 再用例执行完成之后都需要将实际结果和预期结果进行比较(断言),从而断定用例是否通过,unittest 中提供了丰富的断言方法
提供丰富的日志: 当测试用例执行失败会抛出清晰的失败原因,当所有的用例执行完成后会提供丰富的执行结果,执行时间、失败用例数、成功用例数等
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 框架的简单测试脚本
脚本中的类必须继承 unittest.TestCase 类,之后这个类就是一个 TestCase
每一个 TestCase 中都必须含有 setUp 方法和 tearDown 方法
执行测试代码的方法必须以 “test_” 开头
unittest 中提供了 main 方法来执行本测试用例
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()
2. 批量执行测试脚本
1)构建测试套件
将多个测试用例组织起来形成一个 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 中提供了多种方法来构建测试套件
addTest() 方法
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)
makeSuite() 方法
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() 方法
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)
2)用例的执行顺序
unittest 框架在执行多个测试方法时,会根据类名或者方法名的 ASCII 码顺序进行执行,即 0 ~ 9,A ~ Z,a ~ z
就测试方法来说,顺序是根据 “ test_ ” 后的单词进行排序
3)忽略测试用例的执行
如果在某一次测试中不需要执行某一个测试方法,就需要把这个测试方法忽略不执行,使用 @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)
3. unittest 断言
对于每一个单独的测试用例来说,必然会有预期结果和实际结果,通过比对预期结果和实际结果就可以判断测试用例是否通过
反映到代码中就是断言,断言通过则会继续执行下面的代码,否则对应的测试方法就会停止或者生成错误信息,但不会影响其他测试方法的执行
unittest 中提供了丰富的断言方法
msg 参数为断言未通过时的提示语,可以自定义,也可以不写此参数
断言用法小示例:
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="未打开页面")\
未通过就会在控制台给出错误信息
4. HTML 报告生成
脚本执行完成之后,还需要生成一个测试报告,这里就可以使用 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 还会显示详细的错误信息
5. 异常捕获与错误截图
在用例执行时如果可以将错误现场自动截图,那么就会给我们定位错误带来方便
编写一个函数,函数的主要功能就是截图并且保存到指定位置,此函数不以 “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)
常被捕获之后就不会在控制台显示了,我们处理异常的方式是截图错误现场并保存起来
6. 数据驱动
前面我们所有的数据和用例都是写在一起的,但是如果想在同一个用例中测试多个不同数据,按照之前的方法就需要编写多个用例,但其实可以使用 ddt 数据驱动来完成,ddt 可以使一个用例测试多个数据
unittest 没有自带的数据驱动,所以我们需要使用 pip 另外下载 ddt 数据驱动
ddt 中常用的注解:
@ddt: 修饰测试类
@data: 修饰测试方法,参数是测试数据
@file_data: 修饰测试方法,参数是 JSON 文件名,以文件中的数据作为测试数据
@unpack: 当传递的数据是元组或者列表是,使用此注解修饰测试方法,ddt 就会自动将数据映射到参数上
1)测试多个不同数据
一个参数的多个不同值
使用 @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()
2)测试某个文件中的多组数据
txt 文件或者 csv 文件
同样使用使用 @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)
JSON 文件
使用 @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()