unittest自动化测试框架
框架的概念
在系统开发过程中,框架是指对特定应用领域中的应用系统的部分设计和实现子系统的整体结构。
框架将应用系统划分为类和对象,定义类和对象的责任,类和对象如何互相协作,以及对象之间的控制线程。这些共有的设计因素由框架预先定义,应用开发人员只须关注于特定的应用系统特有部分。
自动化测试框架的定义
由一个或多个自动化测试基础模块、自动化测试管理模块、自动化测试 统计模块等组成的工具集合。
- 按框架的定义来分,自动化测试框架可以分为:基础功能测试框架、管理执行框架
- 按不同的测试类型来分,可以分为:功能自动化测试框架、性能自动化测试框架
- 按测试阶段来分,可以分为:单元自动化测试框架、接口自动化测试框架、系统自动化测试框架
- 按组成结构来分,可以分为:单一自动化测试框架、综合自动化测试框架
- 按部署方式来分,可以分为:单机自动化测试框架、分布式自动化测试框架
unittest单元测试框架
熟悉unittest测试框架是后续使用Python进行自动化测试的基础,unittest框架(又名PyUnit框架),为Python语言的单元测试框架。
其官方介绍文档链接为:https://docs.python.org/2.7/library/unittest.html#module-unittest
Unittest测试框架使用介绍:
1、用import语句引入unittest模块
2、让所有执行测试的类都继承于TestCase类,可以将TestCase看成是对特定类进行测试的方法的集合
3、setUp()方法中进行测试前的初始化工作,teardown()方法中执行测试后的清除工作,它们都是TestCase中的方法
4、编写测试的方法最好以test开头(可以直接运行):def test_add(self)、def test_sub(self)等,可以编写多个测试用例对被测对象进行测试
5、在编写测试方法过程中,使用TestCase class提供的方法测试功能点,比如:assertEqual等
6、调用unittest.main()方法运行所有以test开头的方法
应用实例:
对模块中的加法功能测试
存在一个calc.py的模块,里面存在一个加法功能:
def sum(a,b):
return a+b
#encoding: utf-8
import unittest #导入unittest
import calc #导入被测模块
class mytest(unittest.TestCase):
def setUp(self): #初始化工作
pass
def tearDown(self): #退出清理工作
pass
def testsum(self): #具体的测试用例,一定要以test开头
self.assertEqual(calc.sum(1,2),2,"testing sum")
if __name__=="__main__":
unittest.main()
unittest中常用的assert语句
- assertEqual(a, b) a == b
- assertNotEqual(a, b) a != b
- assertTrue(x) bool(x) is True
- assertFalse(x) bool(x) is False
- assertIs(a, b) a is b 2.7
- assertIsNot(a, b) a is not b 2.7
- assertIsNone(x) x is None 2.7
- assertIsNotNone(x) x is not None 2.7
- assertIn(a, b) a in b 2.7
- assertNotIn(a, b) a not in b 2.7
- assertIsInstance(a, b) isinstance(a, b) 2.7
- assertNotIsInstance(a, b) not isinstance(a, b) 2.7
- assertGreater(a, b) a > b 2.7
- assertGreaterEqual(a, b) a >= b 2.7
- assertLess(a, b) a < b 2.7
- assertLessEqual(a, b) a <= b 2.7
unittest创建测试代码的方式
方式一:创建子类继承unittest.TestCase,然后重写runTest方法
class WidgetTestCase(unittest.TestCase):
def setUp(self):
pass
def runTest(self):
pass
def tearDown(self):
pass
方式二:编写以test开头的方法
class WidgetTestCase(unittest.TestCase):
def setUp(self):
pass
def test_xx1(self)
def test_xx2(self)
...
def test_xxN(self)
def tearDown(self):
pass
unittest构建测试套件(测试用例集合)
前提Tester是继承了unittest.TestCase的子类
方式一:
Suite = unittest.TestSuite()
Suite.addTest(Tester('test_default_size'))
Suite.addTest(Tester('test_resize'))
方式二(推荐):
def suite():
suite = unittest.TestSuite()
suite.addTest(Tester('test_default_size'))
suite.addTest(Tester('test_resize'))
return suite
方式三(推荐):
def suite():
tests = ['test_default_size', 'test_resize']
return unittest.TestSuite(map(Tester, tests))
构建测试套件举例:
存在如下类calc:
#encoding: utf-8
class calc(): #计算器类
def __init__(self,a,b): #初始化
self.numa = a
self.numb = b
def sum(self): #加法
return self.numa+self.numb
def sub(self): #减法
return self.numa-self.numb
def multi(self): #乘法
pass
测试方法:
import unittest #导入unittest
import calc
class mytest(unittest.TestCase):
def setUp(self): #初始化工作
self.testnum = calc.calc(3,4)
def tearDown(self): #退出清理工作
del self.testnum
def testsum(self): #具体的测试用例
self.assertEqual(self.testnum.sum(),7,"testing sum")
def testsub(self): #具体的测试用例
self.assertEqual(self.testnum.sub(),-1,"testing sub")
def testmulti(self): #具体的测试用例
self.assertEqual(self.testnum.multi(),12,"testing multi")
def suite():
suite = unittest.TestSuite()
suite.addTest(mytest("testsum"))
suite.addTest(mytest("testsub"))
return suite
if __name__=="__main__":
unittest.main(defaultTest = 'suite')
unittest忽略测试用例
Python 2.7支持忽略部分测试用例不执行,分无条件忽略和有条件忽略, 通过装饰器实现。
使用unitest.skip装饰器族跳过test method或者test class,这些装饰器包括:
@unittest.skip(reason):无条件跳过测试,reason描述为什么跳过测试
@unittest.skipIf(conditition,reason):condititon为true时跳过测试
@unittest.skipUnless(condition,reason):condition不是true时跳过测试
@expected failure:使用@unittest.expectedFailure装饰器,如果test失败了,这个test不计入失败的case数目
举例:
def testsub(self): #具体的测试用例
self.assertEqual(self.testnum.sub(),-1,"testing sub")
@unittest.skip("test skipping") #跳过测试用例
def testmulti(self): #具体的测试用例
self.assertEqual(self.testnum.multi(),12,"testing multi")
运行测试集
unittest使用TestRunner类作为测试用例的基本执行环境,来驱动整个单元测试过程,在单元测试时,一般不直接使用TestRunner类,而是使用其子类TextTestRunner来完成测试,并将结果以文本方式显示出来。
runner=unittest.TextTestRunner()
runner.run(suite)
同时,在unittest模块中定义了一个名为main的全局方法,使用它可以很方便地将一个单元测试模块变成可以直接运行的测试脚本,main()方法使用TestLoader类来搜索所有包含在该模块中的测试方法,并自动执行它们。如果Python程序员能够按照约定(以test开头)来命名所有的测试方法,那么只需要在测试模块的最后加入如下几行代码即可:
if __name__ == "__main__": #__name__为模块内置属性,__main__表示直接执行
unittest.main()
# 或者加参数如下:
unittest.main(defaultTest = 'suite')
批量执行测试用例
通过前面的介绍,我们可以在一个.py文件里面编写多个测试用例,然后执行文件里的所有测试用例,但是如果测试用例数量过多,放一文件里面 就不合理。
比较合理的做法是把相关的几条用例放到一个.py文件里,把所有.py的文件放到一个文件夹下,然后通过一个程序执行文件夹里面所有用例。
如下图所示:
实现方法如下:
baidu.py内容如下:
# -*- coding:utf-8 -*
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
import time,os,unittest
class baidu(unittest.TestCase):
def setUp(self):
Chromedi = "C:\Program Files (x86)\Google\Chrome\Application\chromedriver.exe"
os.environ["webdriver.Chrome.driver"] = Chromedi
self.driver =webdriver.Chrome(Chromedi)
self.baseurl="http://www.baidu.com"
self.verificationErrors=[]
self.accpet_next_alert = True
#测试百度知道链接是否正确
def test_baidu_set(self):
driver =self.driver
driver.maximize_window()
driver.get(self.baseurl+"/")
m=driver.find_element_by_name("tj_briicon")
ActionChains(driver).move_to_element(m).perform()
driver.find_element_by_partial_link_text("知道").click()
# self.assertEqual(driver.current_url,"http://zhidao.baidu.com/")
self.assertEqual(driver.title,u"百度知道 - 全球最大中文互动问答平台")
time.sleep(2)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
search.py内容如下:
# -*- coding:utf-8 -*
from selenium import webdriver
import time,os,unittest
class search(unittest.TestCase):
def setUp(self):
Chromedi = "C:\Program Files (x86)\Google\Chrome\Application\chromedriver.exe"
os.environ["webdriver.Chrome.driver"] = Chromedi
self.driver =webdriver.Chrome(Chromedi)
self.baseurl="http://www.baidu.com"
self.verificationErrors=[]
self.accpet_next_alert = True #测试百度搜索
def test_search(self):
driver =self.driver
driver.maximize_window()
driver.get(self.baseurl+"/")
driver.find_element_by_css_selector("#kw").send_keys("12306")
driver.find_element_by_css_selector("#su1").click()
def tearDown(self):
self.driver.quit()
self.assertEqual([],self.verificationErrors)
if __name__ == "__main__":
unittest.main()
test_case.py内容如下:
# -*- coding:utf-8 -*
import os
caselist = os.listdir('D:\\unittest\\test_case\\')
for a in caselist:
s=a.split('.')[-1]
if s=='py':
os.system('python D:\\unittest\\test_case\\%s 1>>log.txt 2>&1'%a)
caselist获取test_case目录下所有的文件列表做成列表
"baidu.py"为列表的一个元素,用split以"."分割为2个字符串,[-1]表示取出后面py的字符串,判断是否是测试用例文件。如果是用os.system执行Dos命令, Dos命令为执行测试用例并把日志打印到log.txt
至此,一个简单的单元自动化测试框架实现完毕。
生成HTMLTestRunner测试报告
HTMLTestRunner是Python标准库的unittest模块的一个扩展。它能生成易于使用的HTML报告。
1、下载HTMLTestRunner.py文件,地址:http://tungwaiyip.info/software/HTMLTestRunner.html
2、将该文件保存在python安装路径下的lib文件夹中。在文件中能import
HTMLTestRunner成功,即配置成功。
注:如果失败,在项目中新建一个这样的文件也是可以的,只要达到能引入和使用就行。
引入HTML报告后执行测试用例显示如下:
针对baidu.py文件修改如下:
import HTMLTestRunner #导入HTMLTestRunner报告
if __name__ == "__main__":
suite = unittest.TestSuite() #构建测试套件
suite.addTest(baidu("test_baidu_set")) #添加测试用例到套件
filename = 'D:\\unittest\\test_case\\result.html' #建立HTML报告文件
fp = file(filename,'wb')
runner = HTMLTestRunner.HTMLTestRunner(stream=fp,title=u"百度知道测试报告",description=u"用例执行情况:") #自定义测试报告
runner.run(suite) #运行测试用例
说明:
使用HTMLTestRunner配置参数,stream为输出报告路径、title为报告标题、description为描述信息
不同文件中的用例构建测试套件
生成HTMLTestRunner测试报告的时候,会发现一个.py文件产生一个测试报告,那么对于测试用例的多个测试用例文件会产生多个测试报告,这在实际中会导致测试报告不方便阅读;如果把所有测试用例写在一个py文件里,会导致用例不好维护。
之前使用TestSuite只是在一个.py文件里添加多个测试用例,我们可以跨文件通过TestSuite来组织测试用例,实现组织用例结构如下:
测试套件文件解决后,多个文件中的测试用例执行生成的测试报告就会是一份测试报告。
举例一:
import unittest #导入单元测试框架
import asearch,baidu #导入测试用例py文件
alltest = unittest.TestSuite() #构建测试套件
alltest.addTest(unittest.makeSuite(asearch.search)) #增加测试用例集
alltest.addTest(unittest.makeSuite(baidu.baidu)) #增加测试用例集
runner = unittest.TextTestRunner(verbosity=2)
runner.run(alltest) #运行测试
makeSuite用于生产testsuite对象的实例,把所有的测试用例组装成TestSuite,最后把TestSuite传给TestRunner执行,文件结构如下图所示,另一种嵌套测试套件的语法如下:
suite1 = module1.TheTestSuite()
suite2 = module2.TheTestSuite()
alltests = unittest.TestSuite((suite1, suite2))
举例二:整合测试报告
# -*- coding:utf-8 -*
import unittest #导入单元测试框架
import asearch,baidu #导入测试用例py文件
import HTMLTestRunner
alltest = unittest.TestSuite() #构建测试套件
alltest.addTest(unittest.makeSuite(asearch.search)) #增加测试用例集
alltest.addTest(unittest.makeSuite(baidu.baidu)) #增加测试用例集
filename = 'D:\\unittest\\test_case\\result.html'
fp = file(filename,'wb')
runner = HTMLTestRunner.HTMLTestRunner(stream=fp,title=u"百度知道 测试报告",description=u"用例执行情况test:")
runner.run(alltest)
备注:unittest.makeSuite()是直接从一个TestCase的类生成一个TestSuite。
测试套件运行每个测试用例的顺序是由测试方法名根据Python内建函数cmp所排序的顺序而决定的。
测试报告再优化
1、易读性优化:
可以给每个测试用例加上注释:
部分代码如下:
def test_baidu_set(self):
u"""百度知道链接测试"""
driver =self.driver
driver.maximize_window()
在函数后面加函数说明,得出的结果如下图:
2、测试报告名称加时间戳:
引入Python的time模块给测试报告加自定义名称
部分代码如下:
import time
#生成格式化时间
now = time.strftime("%Y-%m-%d_%H_%M_%S",time.localtime(time.time()))
#采取字符串拼接给测试报告加时间
filename = 'D:\\unittest\\test_case\\'+now+'result.html'
生成的报告文件名格式为:2016-01-15_22_15_29result.html
- time.time():当前时间的时间戳
- time.localtime():格式化时间戳为本地的时间
- time.strftime():将时间格式化为字符串
框架结构改进
1、执行用例的主文件移出测试用例文件夹
执行用例的主文件是执行所有测试用例的程序,而并非是测试用例本身,把它移出来结构更为合理。
移出来执行用例的主文件后,会发现import测试用例类报错,这是需要使用Python包。
在test_case下新建一个__init__.py的文件,文件内容可以为空,同时将test_case目录添加到sys.path中,部分代码如下:
import sys sys.path.append("\test_case")
from test_case import baidu,asearch
sys.path.append:把test_case目录添加到path下,使用的是相对路径
当执行用例的主文件和测试用例放置在同一目录下,可以直接调用;移除出来之后就找不到模块了;Python查找模块是先从当前目录下查找,如果找不到再从安装python安装设置的相关路径下去查找,这里使用sys.path目的就是设置路径。
为了标识一个目录是可引用的包,需要在目录下创建__init__.py的文件。
2、init.py文件的使用
一个Python包是一个带有特殊文件 init.py 的目录。init.py 文件定义了包的属性和方法,它控制着包的导入行为。假如 init.py 为空, 那么仅仅导入包是什么都做不了的。
对于 from test_case import baidu,asearch 把baidu和asearch文件导入到测试用例执行主文件,也可以在__init__.py文件实现导入:
在__init__.py中添加如下内容:
import baidu
import asearch
然后在测试用例执行主文件中,可以如下方式调用baidu和asearch文件:
from test_case import *
这样也跟编写代码 from test_case import baidu,asearch 的作用是一致的。
3、把用例中的公共模块移出
首先在test_case目录下创建一个Public目录,然后把一些公共的模块用函数或者类实现,比如:登录模块,退出模块,同时把测试用例做相应修改,测试用例中部分代码如下:
sys.path.append("\Public")
from Public import login
login.login()
代码结构如下图:
用例读取改进
在实际过程中,可能我们需要组织成百上千条测试用例,虽然我们可以通过导入包文件的方式添加测试用例,但每创建一个新的测试用例都需要在测试套件中增加一条addTest语句,随着用例的增加,不便于管理和维护。
解决方法一:
首先把用例文件组装成为一个数组,然后使用for循环遍历的方式读取测试用例文件。示例如下:
testnames = [asearch.search,baidu.baidu]
alltest = unittest.TestSuite() #构建测试套件
for testname in testnames:
alltest.addTest(unittest.makeSuite(testname)) #增加测试用例集
然后,为了在增添或者删除测试用例时不必对执行测试用例的主文件做任何修改,可以把用例列表放置到一个单独的文件中通过执行测试用例的主文件导入。
解决方法二:
使用discover解决用例的读取。在使用解决方法——for循环处理用例读取的时候,如果新增测试用例文件testa.py,那么需要在__init__.py文件中编写 import testa,还需要在测试用例列表文件的数组中增加相应测试用例名称,这样才能使新的测试用例添加到测试套件中执行,这样做显然不方便。
对于上述情况,可以使用TestLoader(测试用例加载器)中的加载多个测试用例方法来实现。
discover函数原型如下:
discover(start_dir, pattern='test*.py', top_level_dir=None)
递归查找指定目录(start_dir)及其子目录下的全部测试模块,将这些测试模块放入一个TestSuite对象并返回。只有匹配pattern的测试文件才会被加载到TestSuite中。
如果一个测试文件的名称符合pattern,将检查该文件是否包含 load_tests() 函数,如果 load_tests() 函数存在,则由该函数负责加载本文件中的测试用例。如果不存在,就会执行loadTestsFromModule(),查找该文件中派生自 TestCase 的类包含的 test 开头的方法。
top_level_dir=None :测试模块的顶级目录(指测试用例不是放在多级目录中),如果没有顶级目录,默认为空
discover使用举例:
test_lists = "D:\\unittest\\test_case"
def createsuite():
testunit = unittest.TestSuite()
discover = unittest.defaultTestLoader.discover(
test_lists,
pattern='ok_*.py',
top_level_dir=None
)
for testsu in discover:
for testcase in testsu:
testunit.addTests(testcase)
return testunit
testnames = createsuite()
runner.run(testnames)
在实际测试用例开发过程中,我们可以使用约定,写好的用例用ok_或者其他标识开头,没有写好的暂时不用ok_开头。不然会影响测试用例的执行