QTA Testbase文档¶
Testbase是所有QTA测试项目的基础,主要提供测试用例管理和执行、测试结果和报告、测试项目管理配置等功能。
使用文档¶
使用前准备¶
Python版本依赖¶
目前QTAF在Python 2.7版本上测试,推荐使用Python2.7版本,如果是Windows系统,推荐使用ActivePython打包的版本。
最新的Mac OSX内置的Python已经为2.7版本,一般无需配置。
创建和修改测试项目¶
对于QTA,一个测试自动化项目指的是针具体产品的特定测试的自动化用例和支持库的集合。在测试项目的支持库中,Testbase是必不可少的基础库,因为Testbase提供了测试项目的基本的管理的支持。
创建测试项目¶
在安装好QTAF后,可以在终端中执行一下命令:
$ qta-manage createproject footestproj
执行成功后,可以看到当前目录下生成一下结构的文件:
/footestproj/
/foolib/
/__init__.py
/testcase.py
/footest/
/__init__.py
/hello.py
/.project
/.pydevproject
/settings.py
/manage.py
导入测试项目到Eclipse¶
如果在Windows/Mac上,可以使用QTA IDE(eclispe)导入以上项目:
- File -> Import... 打开Import对话框
- 选择源类型:General/Existing Projects into Workspace
- 通过Select root directory选择创建的QTA项目的根路径
- 看到Projects窗口显示footestproj,选择并点击Finish完成导入
测试项目结构¶
对于测试项目,一般包括一下的模块:
- 测试用例,比如foo项目中的footest包,这里存储所有的测试用例的脚本。
- 测试业务库,比如foo项目中的foolib包,这里存放所有测试业务Lib层的代码。
- 项目配置文件,即settings.py
- 项目辅助脚本,即manage.py
设计测试用例¶
最简单的测试用例¶
下面我们来编写第一个QTA测试用例,在测试项目中新建一个hello.py:
from testbase.testcase import TestCase
class HelloTest(TestCase):
'''第一个测试用例
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
#---------------------------
self.start_step("第一个测试步骤")
#---------------------------
self.log_info("hello")
可以看到,这个最简单的测试用例包括以下主要的部分:
一个测试用例就是一个Python类,类的名称就是测试用例名,类的DocString就是测试用例的简要说明文档。注意DocString对于QTA测试用例而言是必要的,否则会导致执行用例失败。
测试用例类包括四个必要的属性:
- owner,测试用例负责人,必要属性。
- status,测试用例状态,必要属性。目前测试用例有五种状态:Design、Implement、Review、Ready、Suspend。
- priority,测试用例优先级,必要属性。目前测试用例有四种优先级:BVT、High、Normal和Low。
- timeout,测试用例超时时间,必要属性,单位为分钟。超时时间用于指定测试用例执行的最长时间,如果测试用例执行超过此时间,执行器会停止用例执行,并认为用例执行超时,测试不通过。一般来说,不建议这个时间设置得太长,如果用例需要比较长的执行时间,可以考虑拆分为多个测试用例。
- run_test函数:这个是测试逻辑的代码,每个测试用例只有一个唯一的run_test函数;但每个测试用例可以划分为多个测试步骤,测试步骤以start_step函数调用来分隔。
以上的测试用例并没有任何测试逻辑,只是调用接口打印一行日志。
注解
由于历史的原因,QTA很多接口的函数有两种代码风格的版本,比如上面的run_test、log_info,就有对应的mixedCase的版本runTest、logInfo。一般情况下,建议和测试项目已有的代码使用一致的风格的接口,如果是新项目,推荐使用lower_with_under风格的接口。
调试执行¶
测试用例编写后,需要进行调试执行。需要在hello.py中增加以下的代码:
if __name__ == '__main__':
HelloTest().debug_run()
如果使用的是IDE,在eclipse中,通过“CTRL + F11”快捷键执行当前hello.py脚本,可以看到输出如下:
============================================================
测试用例:HelloTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
----------------------------------------
步骤1: 第一个测试步骤
INFO: hello
============================================================
测试用例开始时间: 2015-04-27 12:51:59
测试用例结束时间: 2015-04-27 12:51:59
测试用例执行时间: 00:00:0.00
测试用例步骤结果: 1:通过
测试用例最终结果: 通过
============================================================
如果没有使用IDE,可以通过manage.py执行单个用例:
$ python manage.py runscript footest/hello.py
在命令行窗口可以看到一样的执行输出:
============================================================
测试用例:HelloTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
----------------------------------------
步骤1: 第一个测试步骤
INFO: hello
============================================================
测试用例开始时间: 2015-04-27 12:51:59
测试用例结束时间: 2015-04-27 12:51:59
测试用例执行时间: 00:00:0.00
测试用例步骤结果: 1:通过
测试用例最终结果: 通过
============================================================
测试用例标签¶
测试用例除了owner、timeout、status和priority之外,还有一个自定义的标签属性“tags”。测试标签的作用是,在批量执行用例的时候,用来指定或排除对应的测试用例,相关详情可以参考《执行测试》。
设置标签的方式十分简单:
from testbase.testcase import TestCase
class HelloTest(TestCase):
'''第一个测试用例
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
tags = "Demo"
def run_test(self):
#---------------------------
self.start_step("第一个测试步骤")
#---------------------------
self.log_info("hello")
标签支持一个或多个,下面的例子也是正确的:
from testbase.testcase import TestCase
class HelloTest(TestCase):
'''第一个测试用例
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
tags = "Demo", "Help"
def run_test(self):
#---------------------------
self.start_step("第一个测试步骤")
#---------------------------
self.log_info("hello")
需要注意的是,测试用例标签经过框架处理后,会变成set类型,比如上面的用例:
assert HelloTest.tags == set("Demo", "Help")
测试环境初始化和清理¶
在前面的例子中,我们在测试用例类的run_test实现了测试的主要逻辑,这里我们引入两个新的接口pre_test和post_test。
假设我们的用例需要临时配置一个本地host域名,示例代码如下:
from testbase.testcase import TestCase
class EnvTest1(TestCase):
'''环境构造测试
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
_add_host("www.qq.com", "11.11.12.12")
# main test logic here
# ...
_del_host("www.qq.com", "11.11.12.12")
以上的代码在逻辑,在用例正常执行完成的情况下是完全正确的,但是这里存在一个问题,就是当run_test测试过程中,由于测试目标bug或者脚本问题导致run_test异常终止,则可能导致host配置没有删除,则可能影响到后面的测试用例。如何解决这个问题呢?QTA为此提供了post_test接口。
下面是使用post_test接口的新版本的测试用例代码:
from testbase.testcase import TestCase
class EnvTest2(TestCase):
'''环境构造测试
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
_add_host("www.qq.com", "11.11.12.12")
# main test logic
# ...
def post_test(self):
super(EnvTest2, self).post_test()
_del_host("www.qq.com", "11.11.12.12")
QTA执行用例的接口是先执行run_test,然后执行post_test;而且即使测试用例执行run_test中发生异常,仍会执行post_test,这样就保证了测试环境的清理操作。
注解
虽然使用post_test可以保证清理环境,但是还是要注意清理环境的逻辑要尽量简单,否则清理环境时发生异常,也会导致清理动作未完成。
和post_test对应,QTA还提供了pre_test接口,从名字上看以看出,pre_test的作用主要是用于测试环境的构造和初始化,下面是使用pre_test的例子:
class EnvTest3(TestCase):
'''环境构造测试
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def pre_test(self):
_add_host("www.qq.com", "11.11.12.12")
super(EnvTest3, self).pre_test()
def run_test(self):
# main test logic
# ...
pass
def post_test(self):
super(EnvTest3, self).post_test()
_del_host("www.qq.com", "11.11.12.12")
QTA会依照以下顺序执行测试用例的三个接口:
- pre_test
- run_test
- post_test
且任意一个接口执行异常,QTA仍然会执行下一个接口。
注解
由于历史原因,QTA还提供另一套代码风格的接口preTest、runTest和postTest,建议测试用例编写时选择测试项目存量代码统一的代码风格,如果是新的测试项目还是建议使用lower_with_under的代码风格。
警告
在一个测试用例中仅支持一套代码风格的接口,QTA选择接口的代码风格是基于run_test/runTest选择的风格为主,也就是说如果用例定义了runTest,则只会执行preTest和postTest,但不会执行pre_test和post_test。当run_test和runTest两个接口都存在的时候,QTA优先选择run_test接口来执行。
pre_test这个接口的一个作用是可以提高测试用例代码的复用,比如以下的例子:
from testbase.testcase import TestCase
class EnvTestBase(TestCase):
def pre_test(self):
super(EnvTestBase, self).post_test()
_add_host("www.qq.com", "11.11.12.12")
def post_test(self):
super(EnvTestBase, self).post_test()
_del_host("www.qq.com", "11.11.12.12")
class EnvTest4(EnvTestBase):
'''环境构造测试
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
# code 1
pass
class EnvTest5(EnvTestBase):
'''环境构造测试
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
# code 2
pass
可以看到EnvTest4和EnvTest5的基类都是为EnvTestBase,也就是他们本身会继承基类的pre_test和post_test的实现,因此也会进行环境的初始化和清理的动作。
注解
可以看到EnvTestBase的pre_test和post_test方法都调用的super接口,对于Python语言的含义表示的是调用基类的方法,虽然不是必定需要的,但是大部分情况下还是推荐这样做;因为这样做可以保证基类的初始化和清理的接口会被执行。
检查测试用例¶
用例测试断言¶
警告
qtaf的新版本,提供了新的断言函数assert_,推荐使用新接口编写用例,assert_equal和assert_match函数建议减少使用。
在测试用例的执行过程中,往往需要设置一些检查点,用于判断用例执行是否符合预期。下面将介绍如何使用断言。
假设我们需要测试一个字符串拼接的函数:
def string_combine(a,b):
return a+b
测试用例的代码如下:
from testbase.testcase import TestCase
class StrCombineTest(TestCase):
'''测试字符串拼接接口
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
#---------------------------
self.start_step("测试字符串拼接")
#---------------------------
result = string_combine("xxX", "yy")
self.assert_("检查string_combine调用结果", result == "xxXyy")
以上的代码执行结果如下:
============================================================
测试用例:StrCombineTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
----------------------------------------
步骤1: 测试字符串拼接
============================================================
测试用例开始时间: 2016-02-02 14:10:21
测试用例结束时间: 2016-02-02 14:10:21
测试用例执行时间: 00:00:0.00
测试用例步骤结果: 1:通过
测试用例最终结果: 通过
============================================================
可以看到结果是测试通过的,但是如果string_combine实现有问题,比如我们新定义一个string_combine:
def string_combine(a,b):
return a +'b'
因为以上的实现是有问题,执行结果必然是不通过的:
============================================================
测试用例:StrCombineTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
----------------------------------------
步骤1: 测试字符串拼接
ASSERT: 检查点不通过:
File "D:\Guying\Workspace\DemoProj\test_assert.py", line 22, in run_test
self.assert_("检查string_combine调用结果", result == "xxXyy")
[检查string_combine调用结果] assert 'xxXb' == 'xxXyy'
============================================================
测试用例开始时间: 2018-08-27 17:00:30
测试用例结束时间: 2018-08-27 17:00:31
测试用例执行时间: 00:00:0.06
测试用例步骤结果: 1:失败
测试用例最终结果: 失败
============================================================
可以看到除了测试不通过外,测试结果还显示了断言失败的代码位置,断言失败的提示信息,以及实际进行断言测试的表达式。
这个就是QTA提供的测试断言的函数接口,其详细的定义如下:
class TestCase(object):
def assert_(self, message, value):
'''断言一个值,为False则测试用例失败
:param message: 断言失败时的提示信息
:param value: 目标值,可以是任何bool结果的表达式或者值,推荐使用表达式
:return: True or False
'''
qtaf从框架层面,提供了assert_断言测试的堆栈信息展示,使用配置项QTAF_REWRITE_ASSERT来控制是否开启,默认是开启的, 在这种情况下,我们可以更加简洁地书写测试断言了,例如我们把原本用例的断言部分整合为一条语句,修改为如下:
self.assert_("检查string_combine调用结果", string_combine("xxX", "yy") == "xxXyy")
那么实际的得到的结果输出如下:
可以看到,堆栈中对应的代码行就是我们书写的那条assert语句,接下来是assert的中间步骤详情:
- 堆栈后续的第一行是断言失败时的提示信息,后面是"assert" + "'xxxXb' == 'xxXyy'"(实际值表达式);
- 后续的行是对第一行实际表达式的值的跟踪,这里的表述意思是, 'xxxXb'是由调用 string_combine('xxX', 'yy')得到的
综上,有了assert_的中间步骤堆栈,我们可以很容易地知道,断言失败是由于string_combine函数调用结果是不正确,不仅代码书写上简单,定位问题速度也更快。
断言失败后置动作¶
QTA测试用例的代码的执行控制逻辑和一般Python的代码是类似的,所以除了执行过程中出现Python异常或用例执行超时,测试用例会一直执行,即使是assert_和wait_for系列的接口失败了,也会继续执行,比如下面的例子:
from testbase.testcase import TestCase
class FlowAfterAssertFailureTest(TestCase):
'''流程控制测试
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
#---------------------------
self.start_step("用例断言失败")
#---------------------------
self.assert_("断言失败", False)
#---------------------------
self.start_step("断言失败后置步骤")
#---------------------------
self.log_info("hello")
上面的第一个测试步骤中,前面一个步骤的断言是必然失败的,但是它不会终止后续步骤执行:
============================================================
测试用例:FlowAfterAssertFailureTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
----------------------------------------
步骤1: 用例断言失败
ASSERT: 检查点不通过:
File "D:\Guying\Workspace\DemoProj\test_assert.py", line 17, in run_test
self.assert_("断言失败", False)
[断言失败] assert False
----------------------------------------
步骤2: 断言失败后置步骤
INFO: hello
============================================================
测试用例开始时间: 2018-08-27 17:03:28
测试用例结束时间: 2018-08-27 17:03:28
测试用例执行时间: 00:00:0.06
测试用例步骤结果: 1:失败 2:通过
测试用例最终结果: 失败
============================================================
注解
对于断言失败的执行逻辑处理,这个是QTA测试框架和其他一般测试框架比较大的差异点,设计测试用例是需要注意。
重试机制¶
注解
对于需要重试的场景,用户都应该使用testbase提供的Retry对象来实现,而不是自己去实现整个重试过程。
对于某些过程,我们可能无法一次性准确保证操作结果符合我们的预期,可能需要通过重试来提高稳定性。 例如,一个表单的UI界面,如果点击“提交”后,我们需要检查“提交”按钮变为不可点击的状态,测试用例可能是这样的:
form.controls['提交按钮'].click()
self.assert_("检查“提交”按钮变为不可点击的状态", form.controls['提交按钮'].enable == False)
这种写法存在的问题是,点击后,到执行assert_断言的时间极短,甚至图标还没有切换到不可点击状态,会导致断言结果不是我们预期的。 因此,我们可能会采取等待一定时间片,然后不断重试的方式:
form.controls['提交按钮'].click()
start = time.time()
while time.time()-start > 2:
if not form.controls['提交按钮'].enable:
break
else:
time.sleep(0.2)
else:
raise RuntimeError("等待超过2秒还是可以点击")
按照上面的实现方式,只要2s内,按钮变成不可点击状态,那么就认为符合预期,因为如果2s后仍然没有切换成不可点击状态,实际上按钮就很可能确实没有切换过去。
还有另一种情况的重试,就是操作的过程耗时比较长,我们更希望按次数去重试,而不是按时间片。例如请求一个网址,由于网络不稳定,我们可能需要重试几次 才能稳定地获取到数据,这个非常简单,使用一个for循环即可。
针对重试,QTAF提供了一个统一的重试机制,同时支持两种场景,便于用户使用重试机制,示例代码如下:
from testbase.retry import Retry
for item in Retry(timeout=2, interval=0.5):
print(item)
得到的输出如下:
<_RetryItem iter=1, ts=1535363022.06>
<_RetryItem iter=2, ts=1535363022.56>
<_RetryItem iter=3, ts=1535363023.06>
<_RetryItem iter=4, ts=1535363023.56>
Traceback (most recent call last):
File "D:\Guying\Workspace\DemoProj\test_assert.py", line 5, in <module>
for item in Retry(timeout=2, interval=0.5):
File "C:\Users\foo\git\qtaf\testbase\retry.py", line 54, in next
raise RetryLimitExcceeded("funtion retried %s times in %ss" % (self.__count, self.timeout))
testbase.retry.RetryLimitExcceeded: function retried 4 times in 2s
上面代码中,我们没有在对应时间内跳出循环,最后重试超出了限制,会抛出RetryLimitExcceeded的异常,我们也可以在构造Retry的时候指定raise_error=False, 这样也不会抛出RetryLimitExcceeded异常。如果我们的代码提前使用break跳出循环,则认为整个重试过程是成功的,不会抛出异常,例如:
from testbase.retry import Retry
for item in Retry(timeout=2, interval=0.5):
break
迭代中,每个item包含两个属性,iteration迭代次数,ts迭代开始的时间戳,如果在迭代中有需要,可以使用这两个属性。 Retry的默认参数是tiemout=10,interval=0.5,使用最大的重试时间来重试。如果指定了limit,则会按照最大重试次数来进行重试,每次重试之间会间隔interval的时间:
from testbase.retry import Retry
for item in Retry(limit=3, interval=0.5):
print(item)
输出结果如下:
<_RetryItem iter=1, ts=1535363744.68>
<_RetryItem iter=2, ts=1535363745.18>
<_RetryItem iter=3, ts=1535363745.68>
Traceback (most recent call last):
File "D:\Guying\Workspace\DemoProj\test_assert.py", line 4, in <module>
for item in Retry(limit=3, interval=0.5):
File "C:\Users\foo\git\qtaf\testbase\retry.py", line 78, in next
raise RetryLimitExcceeded("function retried for %s times" % self.limit)
testbase.retry.RetryLimitExcceeded: function retried for 3 times
类似地,如果在重试次数用完之前,就跳出了循环,也不会抛出RetryLimitExcceeded异常。在了解testbase的重试机制后,我们针对UI按钮的检查代码就可以写成:
for _ in Retry(timeout=2, raise_error=False):
if not form.controls['提交按钮'].enable:
break
else:
self.assert_("按钮没有变为不可点击状态", form.controls['提交按钮'].enable == False)
可以看出,这样写起来就会简洁很多。如果一个目标函数调用在成功的情况下会返回一个bool判定为True的结果,可以直接调用Retry对象的call方法:
result = Retry(limit=2, interval=0.5).call(getattr, (form.controls['提交按钮'], "enable"))
如上,如果符合我们预期,那么result的值会等于True,否则,会抛出RetryLimitExcceeded异常。
忙等待检查¶
在讲解重试机制的时候,我们已经提到了,对于UI操作的检查可以通过Retry来进行检查,testbase提供了wait_for_equal和wait_for_match, 它内部使用到了Retry,用于实现我们所需要的忙等待检查功能,书写可以进一步简化:
form.controls['提交按钮'].click()
self.wait_for_equal("检查提交按钮变为不可点击", form.controls['提交按钮'], "enable", False, timeout=2, interval=0.2)
如果在2s内,form.controls['提交按钮'].enable没有变成False,那么最终用例失败,跟assert_类似,wait_for系列接口失败,用例仍然会继续执行。
执行测试用例¶
对于测试用例的执行,QTA也提供了一定的扩展能力。
重复执行¶
比如需要让一个测试用例重复执行多次:
from testbase.testcase import TestCase, RepeatTestCaseRunner
class RepeatTest(TestCase):
'''测试示例
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
timeout = 1
priority = TestCase.EnumPriority.Normal
case_runner = RepeatTestCaseRunner()
repeat = 2
def run_test(self):
self.log_info('第%s次测试执行' % self.iteration)
if __name__ == '__main__':
HelloTest().debug_run()
这个用例和一般用例的区别是:
- 增加repeat属性,用于指定要重复执行的次数
- case_runner属性,指定了一个“
testbase.testcase.RepeatTestCaseRunner
”实例
直接执行以上代码,输出为:
============================================================
测试用例:RepeatTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
INFO: 第0次测试执行
============================================================
测试用例开始时间: 2015-07-16 20:17:11
测试用例结束时间: 2015-07-16 20:17:11
测试用例执行时间: 00:00:0.00
测试用例步骤结果: 1:通过
测试用例最终结果: 通过
============================================================
============================================================
测试用例:RepeatTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
INFO: 第1次测试执行
============================================================
测试用例开始时间: 2015-07-16 20:17:11
测试用例结束时间: 2015-07-16 20:17:11
测试用例执行时间: 00:00:0.00
测试用例步骤结果: 1:通过
测试用例最终结果: 通过
============================================================
可以看到测试用例被执行了两次,而且每次执行的时候,用例成员变量iteration都会增加1。
控制执行顺序¶
对于一些极端的情况下,需要控制测试用例的执行顺序。比如执行测试用例A、B、C需要按照一定先A、后B、再C的顺序来执行。
警告
QTA不推荐测试用例之间存在依赖关系,这样对于用例的可读性和后续的维护都会带来麻烦,所以不推荐控制用例按照顺序来执行。
例如下面一个控制执行顺序的例子:
from testbase import TestCase
from testbase.testcase import RepeatTestCaseRunner
class TestA(TestCase):
'''测试示例
'''
timeout = 1
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
def run_test(self):
pass
class TestB(TestCase):
'''测试示例
'''
timeout = 1
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
case_runner = RepeatTestCaseRunner()
repeat = 2
def run_test(self):
pass
class TestC(TestCase):
'''测试示例
'''
timeout = 1
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
def run_test(self):
pass
__qtaf_seq_tests__ = [TestA, TestB, TestC]
if __name__ == '__main__':
from testbase.testcase import debug_run_all
debug_run_all()
以上用例和普通的用例完全一致,不一样的地方是在模块中定义了变量qtaf_seq_tests ,这个变量就是用来指定测试用例的执行顺序。需要注意的是,如果要指定测试用例按照顺序执行,这些用例的实现都必须放在同一个代码文件中,这样限制的目的是为了提高代码的可读性。
以上的例子的执行结果如下:
============================================================
测试用例:TestA 所有者:foo 优先级:Normal 超时:1分钟
============================================================
============================================================
测试用例开始时间: 2015-07-16 20:24:46
测试用例结束时间: 2015-07-16 20:24:46
测试用例执行时间: 00:00:0.00
测试用例步骤结果: 1:通过
测试用例最终结果: 通过
============================================================
============================================================
测试用例:TestB 所有者:foo 优先级:Normal 超时:1分钟
============================================================
============================================================
测试用例开始时间: 2015-07-16 20:24:46
测试用例结束时间: 2015-07-16 20:24:46
测试用例执行时间: 00:00:0.00
测试用例步骤结果: 1:通过
测试用例最终结果: 通过
============================================================
============================================================
测试用例:TestB 所有者:foo 优先级:Normal 超时:1分钟
============================================================
============================================================
测试用例开始时间: 2015-07-16 20:24:46
测试用例结束时间: 2015-07-16 20:24:46
测试用例执行时间: 00:00:0.00
测试用例步骤结果: 1:通过
测试用例最终结果: 通过
============================================================
============================================================
测试用例:TestC 所有者:foo 优先级:Normal 超时:1分钟
============================================================
============================================================
测试用例开始时间: 2015-07-16 20:24:46
测试用例结束时间: 2015-07-16 20:24:46
测试用例执行时间: 00:00:0.00
测试用例步骤结果: 1:通过
测试用例最终结果: 通过
============================================================
自定义执行方式¶
对于一般的测试用例的执行,QTA是按照下面的流程处理的:
- 获取尝试测试用例对应的case_runner静态变量,如果不存在,则设置case_runner为一个“
testbase.testcase.TestCaseRunner
”实例- 使用case_runner去执行对应的用例
因此,每个测试用例都可以通过指定这个case_runner来重载用例的执行逻辑。前面的重复执行用例的例子,就是通过“testbase.testcase.RepeatTestCaseRunner
”来实现的。
测试用例指定的case_runner要符合一定的接口规范,这个接口就是“testbase.testcase.ITestCaseRunner
”,其定义如下:
class ITestCaseRunner(object):
def run(self, testcase, testresult_factory ):
"""执行一个用例
:param testcase: 测试用例
:type testcase: TestCase
:param testresult_factory: 测试结果对象工厂
:type testresult_factory: ITestResultFactory
:rtype: TestResult/TestResultCollection
"""
pass
下面以一个例子来示例如果重载case_runner来指定一个测试用例执行的时候重复执行多次,也就是实现一个我们自己的版本的RepeatTestCaseRunner:
from testbase.testresult import TestResultCollection
from testbase.testcase import ITestCaseRunner, TestCaseRunner
class RepeatTestCaseRunner(ITestCaseRunner):
def run(self, testcase, testresult_factory ):
passed = True
results = []
for _ in range(testcase.repeat):
result = TestCaseRunner().run(testcase, testresult_factory)
results.append(result)
passed &= result.passed
if not passed: #有一次执行不通过则中止执行
break
return TestResultCollection(results, passed)
处理测试结果¶
对于一个测试用例,其测试结果包括这个测试用例的执行通过与否,和对应的日志信息。
测试通过与不通过¶
对于QTA,判断一个测试用例是否通过的原则:
- 测试用例类定义有问题,比如缺少DocString或必要属性,缺少run_test接口的实现。
- 如果测试用例类定义正确,则当所有测试步骤都通过时,测试用例测试通过,否则测试用例不通过”。
而判断一个测试步骤是否通过,主要看是否出现以下任意一个情况:
- 测试断言失败,即调用assert或wait_for系列的接口检查不通过
- 测试代码问题,Python代码执行异常
- 测试执行过程中,QTA内置的logger有错误级别的日志
第一种情况在前面《设计测试用例》章节已经有介绍,对于后面的两种情况,我们看下面的例子:
class ExceptTest(TestCase):
'''异常测试
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
#---------------------------
self.start_step("异常测试")
#---------------------------
raise RuntimeError("抛异常")
以上的用例有问题,执行比如有会有异常抛出,因此测试结果是不通过的:
============================================================
测试用例:ExceptTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
----------------------------------------
步骤1: 异常测试
CRITICAL: run_test执行失败
Traceback (most recent call last):
File "D:\workspace\qtaf5\testbase\testcase.py", line 550, in _thread_run
getattr(self._testcase, it)()
File "D:\workspace\qtaf5\test\hellotest.py", line 86, in run_test
raise RuntimeError("抛异常")
RuntimeError: 抛异常
============================================================
测试用例开始时间: 2016-02-02 15:12:03
测试用例结束时间: 2016-02-02 15:12:03
测试用例执行时间: 00:00:0.02
测试用例步骤结果: 1:失败
测试用例最终结果: 失败
============================================================
再看看一个测试不通过的用例的例子:
class LogErrorTest(TestCase):
'''异常测试
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
#---------------------------
self.start_step("异常测试")
#---------------------------
self.fail("异常发生")
if __name__ == '__main__':
ExceptTest().debug_run()
上面的用例是调用日志的接口,记录一个错误的日志,因此测试也不通过:
============================================================
测试用例:LogErrorTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
----------------------------------------
步骤1: 异常测试
ERROR: 异常发生
============================================================
测试用例开始时间: 2016-02-02 15:14:19
测试用例结束时间: 2016-02-02 15:14:19
测试用例执行时间: 00:00:0.01
测试用例步骤结果: 1:失败
测试用例最终结果: 失败
============================================================
测试日志¶
从前面测试用例的例子可以看到,测试结果主要包括几类信息:
- 测试用例基本信息,如名称、负责、优先级等
- 测试用例执行的基本信息,比如开始时间、结束时间
- 测试用例执行结果,通过或不通过
- 各个测试步骤的日志信息,包括测试步骤的名称、测试步骤通过与否,和测试步骤执行过程中的日志、断言失败信息等
前面三点的信息都是固定的,第四点的信息是基于测试用例的代码而变化的,像一些特殊的日志信息,比如断言失败的日志,会由用户的assert或wait_for接口产生。但是一般来说,用户可以通过下面两个接口记录日志:
def log_info(self, info ):
'''Log一条信息
:type info: string
:param info: 要Log的信息
'''
def fail(self, message):
'''测试用例失败
:type message: string
:param message: 要Log的信息
'''
以上两个接口在《设计测试用例》章节已经有介绍,从使用上,这两个接口只能在测试用例类的方法中使用,如果需要在测试用例之外的代码,比如lib层,则可以使用QTA内置的logger:
from testbase import logger
logger.info("hello")
logger.error("error")
上面的代码等价于在测试用例中使用log_info和fail:
self.log_info("hello")
self.fail("error")
QTA内置的logger的接口和Python标准库的logging的logger是完全兼容的。
测试结果对象¶
对于一个测试用例对象,在执行过程中都会有一个test_result属性表示此测试用例对应的测试结果,我们也可以通过这个测试结果对象的接口去记录日志信息:
self.test_result.info("hello")
self.test_result.error("error")
上面的代码等价于在测试用例中使用log_info和fail:
self.log_info("hello")
self.fail("error")
test_result属性返回的类型为“testbase.testresult.TestResultBase
”,更多接口可以参考接口文档。
test_result的日志接口,无论info、error等,其实都是调用log_record实现,比如info的接口:
def info(self, msg, record=None, attachments=None):
'''处理一个INFO日志
'''
self.log_record(EnumLogLevel.INFO, msg, record, attachments)
可以看到这里其实有两个另外的参数:record和attachments。record主要是给用户传递自定义的参数给自定义的测试结果对象,这块会在《执行测试》中讨论。而atachments参数表示的是测试用例的附加文件信息,比如截图、Dump文件或日志文件等。
下面是使用attachments参数的例子:
self.test_result.info("这个是一个截图", attachments={"PC截图":"desktop.png"})
调试执行的结果:
INFO: 这个是一个截图
PC截图:desktop.png
attachments参数是一个字典,因此也支持多个附件:
self.test_result.info("这个是全部截图", attachments={"PC截图":"desktop.png", "手机截图":"mobile.png"})
在调试执行是,附件的日志信息意义其实不大,但是对于其他执行方式,如果采用不同的测试结果格式(比如xml、网页报告),测试附件会直接附加在对应的测试结果中,方便用户分析测试用例问题。这块会在《执行测试》中讨论,这里也不展开讨论。
测试日志的级别¶
test_result的log_record接口第一个参数就是日志级别,比如对于info接口,其对应的日志的级别就是INFO。以下是test_result目前支持的全部日志级别信息:
class EnumLogLevel(object):
'''日志级别
'''
DEBUG = 10
INFO = 20
Environment = 21 #测试环境相关信息, device/devices表示使用的设备、machine表示执行的机器
ENVIRONMENT = Environment
WARNING = 30
ERROR = 40
ASSERT = 41 #断言失败,actual/expect/code_location
CRITICAL = 60
APPCRASH = 61 #测试目标Crash
TESTTIMEOUT = 62 #测试执行超时
RESNOTREADY = 69 #当前资源不能满足测试执行的要求
其中,INFO/WANRING/ERROR/CRITICAL的类型都是和Python的logging模块的日志级别对应的,是一般的日志级别。除此之外,ASSERT是在断言失败的时候使用,也就是wait_for_和assert_系列结果中使用,用户不用直接使用。TESTTIMEOUT和RESNOTREADY也是内置的类型,由测试框架调用,用户一般都不用使用。用户可以使用的剩下的两个特殊的日志级:ENVIRONMENT和APPCRASH。
ENVIRONMENT用于日志环境信息,比如测试用例使用PC、手机等信息,比如:
self.test_result.log_record(EnumLogLevel.ENVIRONMENT, "测试用例执行机名称", {"machine":socket.gethostname()})
self.test_result.log_record(EnumLogLevel.ENVIRONMENT, "使用移动设备", {"device":"01342300111222"})
self.test_result.log_record(EnumLogLevel.ENVIRONMENT, "使用移动设备", {"devices":["93284972333", "21903948324923"]})
APPCRASH用于记录被测对象的Crash,比如:
self.test_result.log_record(EnumLogLevel.APPCRASH, "QQ Crash", attachments={"QQ日志": "QQ.tlg", "QQ Dump": "QQ34ef450a.dmp"})
注解
ENVIRONMENT和APPCRASH约定的record参数类型并不是强制的,但是如果希望日志被内置的测试结果类型更好的处理,需要按照其约定来调用。
异常时日志¶
测试用例执行过程中有两种可能的异常情况,用例执行超时或者用例测试代码异常。在这种情况下,QTA一般会记录当时的堆栈信息,但是如果需要在这种情况增加更多的信息,比如当时的截图、环境信息等,则可以使用测试用例类的get_extra_fail_record接口。示例代码如下:
class EnvLogOnExceptTest(TestCase):
'''异常时记录IP和时间
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
raise RuntimeError("异常")
def get_extra_fail_record(self):
record, attachments = super(EnvLogOnExceptTest, self).get_extra_fail_record()
record['当前IP'] = socket.gethostbyname(socket.gethostname())
attachments['当前代码文件'] = __file__
return record, attachments
get_extra_fail_record主要是提供一个hook点,可以在日志异常信息时,让测试用例去修改record和attachments参数。上面的例子就是在record和attachments增加了两项内容。
get_extra_fail_record是在日志级别为ERROR或者以上时被执行,也就是包括:
- self.fail、logger.error和test_result.error产生ERROR级别的日志时
- self.assert_和self.wait_for_系列接口断言失败时
- 测试用例执行超时时
- 测试用例执行异常时
管理测试用例¶
从《设计测试用例》部分可以我们知道,一个测试用例,就是一个Python的测试用例类。在实际的测试项目中,会包含成百上千的测试用例,所以需要一种组织形式来管理这些测试用例。
组织形式¶
一个测试用例,就是一个Python的测试用例类,因此测试用例的组织其实就是Python类的组织。对于Python而言,存在三层结构的代码组织形式:包、模块和类。
Python模块即对应一个py代码文件,比如前面的hello.py就是定义一个python的模块。Python的包是一个模块的容器,Python要求包中至少定义一个__init__.py的模块,而且Python包是允许包含另一个Python包,因此可以构成一个N层次的树状结构。例如下面的代码组织形式:
zootest\
__init__.py
cat\
__init__.py
feed.py *
play.py
dog\
__init__.py
feed.py
play.py
Python以名字来唯一表示一个模块,也就是说,名字相同的模块就是同一个模块,所以模块名字必须是唯一的。使用“.”间隔的方式来唯一定位一个模块,比如上面的代码树的例子中加“*”的模块的名字如下:
zootest.cat.feed
因此,对应的在feed.py里面的类的名字的唯一标识为:
zootest.cat.feed.FeedFishTest
zootest.cat.feed.FeedMouseTest
zootest.cat.feed.FeedAppleTest
由于一个测试用例,就是一个Python的测试用例类,所以测试用例的名字也就和类的名字是一样的(数据驱动用例除外)。
注解
Python初学者容易忘记在包定义中增加__init__.py文件,如果没有__init__.py,则对于Python来说只是一个普通的文件夹,因此定义在里面的测试用例也无法被QTA识别出来。
加载测试用例¶
对于一个测试项目中大量的测试用例,我们可以使用TestLoader来加载和分析,例如下面的代码:
from testbase.loader import TestLoader
loader = TestLoader()
for it in loader.load("zootest"):
print(it)
上面代码是加载zootest包下面的全部测试用例,并展示其对应的测试用例名称,执行的结果如下:
zootest.cat.feed.FeedFishTest
zootest.cat.feed.FeedMouseTest
zootest.cat.feed.FeedAppleTest
zootest.cat.play.PlayBallTest
zootest.cat.play.PlayLightTest
zootest.dog.feed.FeedFishTest
zootest.dog.feed.FeedMouseTest
zootest.dog.feed.FeedAppleTest
zootest.dog.play.PlayBallTest
zootest.dog.play.PlayLightTest
TestLoader的load可以接受非顶层的包名,比如:
for it in loader.load("zootest.cat"):
print(it)
返回:
zootest.cat.feed.FeedFishTest
zootest.cat.feed.FeedMouseTest
zootest.cat.feed.FeedAppleTest
zootest.cat.play.PlayBallTest
zootest.cat.play.PlayLightTest
也支持模块名:
for it in loader.load("zootest.cat.feed"):
print(it)
返回:
zootest.cat.feed.FeedFishTest
zootest.cat.feed.FeedMouseTest
zootest.cat.feed.FeedAppleTest
甚至可以支持测试用例名:
for it in loader.load("zootest.cat.feed.FeedFishTest"):
print(it)
返回:
zootest.cat.feed.FeedFishTest
可以看到通过不同的层次路径,我们可以控制测试用例的范围。如果通过名字控制的方式比较难筛选,也可以通过过滤函数来筛选:
def filter( testcase ):
if testcase.status != TestCase.EnumStatus.Ready:
return "status is not ready"
loader = TestLoader(filter)
for it in loader.load("zootest"):
print(it)
以上的代码可以过滤掉全部状态不是为Ready的测试用例。如果需要查询被过滤的全部测试用例,可以调用下面接口:
filtered_records = loader.get_filtered_tests_with_reason()
for tc in filtered_records:
print(tc.name, filtered_records[tc])
处理加载失败¶
测试用例加载过程中,可能会遇到由于测试脚本设计问题,在加载模块的时候就异常了,比如下面的py脚本:
from testbase.testcase import TestCase
raise RuntimeError("load error")
class HelloTest(TestCase):
'''测试示例
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
timeout = 1
priority = TestCase.EnumPriority.Normal
def runTest(self):
pass
上面的脚本加载必然失败,TestLoader会把这种错误记录下来,通过下面的方式可以查询:
err_records = loader.get_last_errors()
for name in err_records:
print 'name:', name
print 'error:', err_records[name]
执行的结果:
name: hello
error: Traceback (most recent call last):
File "D:\workspace\qtaf5\test\hellotest.py", line 14, in <module>
raise RuntimeError("load error")
RuntimeError: load error
数据驱动测试¶
所谓数据驱动测试,即是将测试数据和测试用例分离,可以实现一份测试用例跑多份的测试数据,在一些业务场景的测试下可以大大提升测试用例开发和维护的成本。
数据驱动测试用例¶
先看一个简单的例子,小E需要设计一个针对登录的测试用例:
- 输入“111”登录QQ,登录失败,提示非法帐号
- 输入“”登录QQ,登录失败,提示非法帐号
- 输入“11111111111111111”登录QQ,登录失败,提示非法帐号
- 输入“$%^&#”登录QQ,登录失败,提示非法帐号
如果需要实现对应的自动化测试用例,则可能会写类似下面的代码:
from testbase.testcase import TestCase
class InvalidUinTest1(TestCase):
'''非法测试号码1
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
qq = QQApp()
login = LoginPanel(qq)
login['uin'] = "111"
login['passwd'] = "test123"
login['login'].click()
self.assertEqual(login['tips'].text, "非法帐号")
class InvalidUinTest2(TestCase):
'''非法测试号码2
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
qq = QQApp()
login = LoginPanel(qq)
login['uin'] = ""
login['passwd'] = "test123"
login['login'].click()
self.assertEqual(login['tips'].text, "非法帐号")
class InvalidUinTest3(TestCase):
'''非法测试号码3
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
qq = QQApp()
login = LoginPanel(qq)
login['uin'] = "11111111111111111"
login['passwd'] = "test123"
login['login'].click()
self.assertEqual(login['tips'].text, "非法帐号")
class InvalidUinTest4(TestCase):
'''非法测试号码4
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
qq = QQApp()
login = LoginPanel(qq)
login['uin'] = "$%^&#"
login['passwd'] = "test123"
login['login'].click()
self.assertEqual(login['tips'].text, "非法帐号")
if __name__ == '__main__':
InvalidUinTest1().debug_run()
InvalidUinTest2().debug_run()
InvalidUinTest3().debug_run()
InvalidUinTest4().debug_run()
从上面的代码看出,用户的逻辑基本上是类似的,每个用例几乎只有一点点的差异,特别是如果测试的场景变多了,用例维护起来更麻烦。
这里我们就可以用数据驱动用例来解决这个问题,使用数据驱动修改后的用例:
from testbase.testcase import TestCase
from testbase import datadrive
testdata = [
"111",
"",
"11111111111111111",
"$%^&#",
]
@datadrive.DataDrive(testdata)
class InvalidUinTest(TestCase):
'''非法测试号码
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
qq = QQApp()
login = LoginPanel(qq)
login['uin'] = self.casedata
login['passwd'] = "test123"
login['login'].click()
self.assertEqual(login['tips'].text, "非法帐号")
if __name__ == '__main__':
InvalidUinTest().debug_run()
如果执行以上的代码,其输出的结果是和前面四个测试用例的执行结果是一致的,但是这里却只有一个用例,这个就是数据驱动测试用例的强大之处。
上面的数据驱动测试用例和一般测试用例的主要区别在两点:
- 测试用例类增加了修饰器“
testbase.datadrive.DataDrive
”,修饰器接受一个参数来指定对应的测试数据- 测试用例通过casedata属性获取测试数据
测试数据¶
测试数据通过修饰器“testbase.datadrive.DataDrive
”指定,目前支持两个格式的数据:
- list和list兼容类型
- dict和dict兼容类型
list类型测试数据¶
上面的InvalidUinTest使用的就是list类型的测试数据,对于list类型的数据,QTA会将list的每一个元素生成对应的一个测试用例,并将该元素赋值给对应的测试用例的casedata属性。
例如测试数据为:
@datadrive.DataDrive(["AA", 1234234, {"xx":"XX"}, True])
class HelloDataTest(TestCase):
pass
则生成的四个测试用例对应的casedata分别为:
"AA"
1234234
{"xx":"XX"}
True
dict类型测试数据¶
数据驱动也支持dict类型的测试数据,QTA会讲dict类型的所有值生成对应的一个测试用例,并将该值赋给对应的测试用例的casedata属性。
例如测试数据为:
@datadrive.DataDrive({
"A": "AA",
"B": 1234234,
"C": {"xx":"XX"},
"D": True
})
class HelloDataTest(TestCase):
pass
则生成的四个测试用例对应的casedata分别为:
"AA"
1234234
{"xx":"XX"}
True
但dict的键在这里似乎没什么用处?
调试数据驱动用例¶
数据驱动用例的调试时,可以和一般用例一样使用debug_run接口,例如:
if __name__ == '__main__':
HelloDataTest().debug_run()
使用debug_run调试时,会执行全部数据驱动的用例,如果需要针对单个数据进行调试,可以使用debug_run_one接口:
if __name__ == '__main__':
HelloDataTest().debug_run_one()
以上的待会随机使用一个数据驱动用例进行执行调试,如果需要指定某个数据的用例进行调试:
if __name__ == '__main__':
HelloDataTest().debug_run_one("B")
这里的“B”是数据驱动的数据的名字,按照以上HelloDataTest的定义,的对应的数据就是1234234。
设置单个数据驱动用例属性¶
由于数据驱动方式生成的多个测试用例,虽然测试数据不同,但是由于都是同一个Python类,因此都具有相同的测试用例属性(优先级、状态、描述等)。 如果需要设置单个数据对应的生成的用例的属性,则要求测试数据必须是dictionary的类型,测试用例的属性可以在dictionary数据中的“__attrs__”键值来定义:
@datadrive.DataDrive([
{"data": 121231, "__attrs__": {"priority": TestCase.EnumPriority.Low } },
{"data": True, "__attrs__": {"__doc__": "使用布尔类型数据测试"} },
])
class HelloDataTest(TestCase):
"""示例用例
"""
owner = "xxx"
timeout = 1
priority = TestCase.EnumPriority.BVT
status = TestCase.EnumStatus.Ready
def run_test(self):
self.log_info(self.priority)
self.log_info(self.test_doc)
以上的用例,在根据测试用例生成的用例,会具备不一样的属性,比如第一个数据驱动用例的优先级会变为“TestCase.EnumPriority.Low”,但第二个数据对应的用例则继承原本用例类的优先级“TestCase.EnumPriority.BVT”。
除了上面例子中的属性,用户可以设置以下的用例属性:
* priority
* status
* owner
* timeout
* tags
* __doc__
注解
此方式不支持全局数据驱动
管理数据驱动测试用例¶
QTA对于每个测试用例,都有一个唯一的名字;由于数据驱动把一个测试用例对应数据生成了多个测试用例,所以QTA对于每个数据驱动生成的用例的名字也是不一样的。
假设一个数据驱动的用例footest/cat/eat.py:
@datadrive.DataDrive(["fish", "mouse", "apple"])
class EatTest(TestCase):
#这里省略相关代码
pass
如果我们参考《管理测试用例》使用TestLoader来加载这块测试用例:
from testbase.loader import TestLoader
loader = TestLoader()
for it in loader.load("zootest.cat.eat"):
print(it.test_name)
执行结果如下:
zootest.cat.eat.EatTest/0
zootest.cat.eat.EatTest/1
zootest.cat.eat.EatTest/2
可以看到每个用例后面都有一个后缀,表示对应的list的索引值。
这个是list类型的例子,如果是dict类型:
@datadrive.DataDrive({
"fish": "fish",
"mouse": "mouse",
"apple": "apple",
})
class EatTest(TestCase):
#这里省略相关代码
pass
则TestLoader的执行结果如下:
zootest.cat.eat.EatTest/fish
zootest.cat.eat.EatTest/mouse
zootest.cat.eat.EatTest/apple
之前的list的索引变成了dict键。
其实TestLoader也支持加载一个单独的数据驱动用例:
from testbase.loader import TestLoader
loader = TestLoader()
for it in loader.load("zootest.cat.eat/fish"):
print(it.test_name)
则TestLoader的执行结果如下:
zootest.cat.eat.EatTest/fish
全局数据驱动测试¶
数据驱动用例需要我们去修改测试用例,并为每个测试用例都增加修饰器和通过casedata访问数据,但是有没有可能在不修改测试用例的情况下,对全部的测试用例都进行数据驱动测试呢?比如对于后台测试,通过配置一份测试服务器的IP列表作为测试数据,然后对全部用例都以这份IP列表来生成对应的N个用例。答案就是全局数据驱动用例。
设置全局数据驱动需要修改项目的settings.py文件,增加下面两个配置:
DATA_DRIVE = True
DATA_SOURCE = 'test/data/server.py'
注解
settings.py配置的更多使用方法,请参考《配置测试项目》
第一个配置表示打开全局数据驱动,第二个配置指定一个py文件作为数据源,如server.py:
DATASET = [
"11.22.11.11",
"11.22.11.12",
"11.22.11.13",
"11.22.11.14",
]
数据py文件只需要定义一个DATASET模块变量,变量类型要求是list或者dict类型,格式和作用和前面的数据驱动用例DataDrive参数是一样的。
通过以上配置之后,本地调用debug_run调试脚本,可以看到每个用例都会被执行4次,且每次的casedata数据分别为DATASET变量定义的数据。
如果数据格式比较简单,也可以直接内嵌在settings.py中,这个时候DATA_SOURCE即表示数据源,同上面配置等价的配置如下:
DATA_DRIVE = True
DATA_SOURCE = [
"11.22.11.11",
"11.22.11.12",
"11.22.11.13",
"11.22.11.14",
]
注解
当测试用例已经有修饰器DataDrive,但同时配置了全局数据驱动,这个时候全局数据驱动对于这个用例是无效的,这个用例还是只会通过DataDrive生成数据驱动测试用例。
执行测试¶
命令行执行测试¶
警告
在执行用例前,我们需要在命令行先cd到工程的根目录(即包含manage.py的那个目录)。
本节主要介绍如何批量执行所有的测试用例并生成对应的测试报告。命令行执行用例都是基于runtest命令实现的。因此,所有执行 用例相关的命令行是以python manage.py runtest开头,查看命令行帮助,可以执行:
$ python manage.py runtest -h
指定用例集¶
命令行执行用例跟qta平台上一样,是通过用例集来加载目标用例集合的,一个用例集是一个用句点分隔的字符串, 句点分隔的条目,每部分都可以是python的模块路径,最后一部分可以是用例名。多个用例集使用空格隔开。
例如:
$ python manage.py runtest zoo.test foo bar # 执行zoo.test、foo、bar三个用例集
$ python manage.py runtest zoo.test.HelloTest # 执行zoo.test模块下HelloTest用例
$ python manage.py runtest zoo.test # 执行zoo.test模块下所有用例,包括HelloTest等用例
$ python manage.py runtest zoo # 执行zoo模块下所有用例,包括test等子模块下的所有用例
使用\--excluded-name选项,可以排除用例集合,接受多个排除用例集合,例如:
$ python manage.py runtest zoo --excluded zoo.test # 执行zoo模块下所有用例,但是排除zoo.test
$ python manage.py runtest zoo --excluded zoo.xxxx --excluded zoo.oooo #排除zoo.xxxx和zoo.oooo
指定工作目录¶
使用-w或\--working-dir可以指定执行用例的工作目录,相关的输出文件也会放到工作目录:
$ python manage.py runtest -w foo zoo
$ python manage.py runtest --working-dir foo zoo
如果没有指定工作目录,会通过os.getcwd()获取,所以通常来说,就是manage.py所在的目录。
指定工作目录可以是绝对路径,也可以是相对路径。如果是相对路径,则相对于当前工作路径而言。
指定用例优先级¶
使用\--priority可以根据优先级过滤用例,多个\--priority选项可以指定多个优先级, 可选的用例优先级为:BVT、High、Normal、Low,例如:
$ python manage.py runtest zoo --priority BVT --priority Normal
如果不指定优先级,所有优先级的用例都可以被执行。
指定用例状态¶
使用\--status可以根据用例状态过滤用例,多个\--status选项可以指定多个状态, 可选的用例状态为:Design、Implement、Ready、Review、Suspend,例如:
$ python manage.py runtest zoo --status Design --status Ready
如果不指定状态,除Suspend以外的所有状态的用例都可以被执行。
指定用例作者¶
使用\--owner可以根据用例作者过滤用例,多个\--owner选项可以指定多个owner,例如:
$ python manage.py runtest zoo --owner guying
指定用例标签¶
使用\--tag和\--excluded-tag可以根据用例标签过滤用例,多个\--tag可以指定多个标签, 多个\--excluded-tag可以排除多个标签,例如:
$ python manage.py runtest zoo --tag foo --excluded-tag bar
指定测试报告类型¶
测试报告类型的选项:
- \--report-type,报告类型,可以是xml、json、empty、stream、online,默认是stream。
- \--report-args,传递给测试报告对象的命令行参数,需要使用双引号引用起来,并且尾部至少需要保留一个空格,具体支持的参数可以通过帮助信息查看。
- \--report-args-help,打印指定报告类型的命令行参数帮助信息。
如果我们想要查看某个测试报告类型所支持的参数,可以使用命令行来打印:
$ python manage.py --report-args-help stream
usage: runtest <test ...> --report-type <report-type> [--report-args "<report-args>"]
optional arguments:
-h, --help show this help message and exit
--no-output-result don't output detail result of test cases
--no-summary don't output summary information
xml类型,会生成xml格式的报告文件,输出到工作目录下,可以用浏览器打开TestReport.xml查看报告内容, windows下会自动通过IE打开。无命令行参数。
json类型,会生成json格式的报告文件,输出到stdout或指定文件路径。命令行参数如下:
- \--name,测试报告的标题;
- \--output,输出文件路径,默认是stdout。
empty类型,将不输出报告内容。无命令行参数。
stream类型,将报告内容输出到stdout,与调试用例时debug_run输出的信息一致。命令行参数如下:
- \--no-output-result,指定后,用例执行的中间内容将不会输出到报告;
- \--no-summary,指定后,将不输出用例执行统计结果。
例如:
$ python manage.py runtest --report-args-help stream
$ python manage.py runtest zoo --report-type stream --report-args "--no-output-result --no-summary"
$ python manage.py runtest zoo --report-type xml -w test_result
指定资源管理后端¶
可以通过\--resmgr-backend-type指定资源管理后端的类型,目前仅支持local,可以满足绝大部分的项目测试需求。
例如:
$ python manage.py runtest zoo --resmgr-backend-type local
指定用例执行器¶
测试用例执行器相关的选项:
- \--runner-type,用例执行器TestRunner的类型,目前支持multithread,multiprocess,basic。
- \--runner-args,传递给TestRunner的命令行参数,需要使用双引号引用起来,并且尾部至少需要保留一个空格,具体的参数信息可以通过帮助信息查看。
- \--runner-args-help,打印指定类型的TestRunner的命令行参数信息。
如果我们想要某个执行器类型支持的参数,可以通过下面命令打印:
$ python manage.py runtest --runner-args-help basic
usage: runtest <test ...> --runner-type <runner-type> [--runner-args "<runner-args>"]
optional arguments:
-h, --help show this help message and exit
--retries RETRIES retry count while test case failed
multithread类型,使用多线程来并发执行用例。命令行参数如下:
- \--retries,用例失败后的最大重试次数,默认为0,不重试。
- \--concurrency,用例执行的并发数,默认为0,使用当前cpu核数作为并发数。
multithread类型,使用多进程来并发执行用例。命令行参数如下:
- \--retries,用例失败后的最大重试次数,默认为0,不重试。
- \--concurrency,用例执行的并发数,默认为0,使用当前cpu核数作为并发数。
basic类型,只能以单个串行方式执行用例,适合调试单个用例的场景。命令行参数如下:
- \--retries,用例失败后的最大重试次数,默认为0,不重试。
自定义代码执行测试¶
上面内容都是通过manage.py runtest来执行测试用例,如果想要自己定制执行用例过程,可以通过QTA的接口来执行测试用例。
如果用户想要自己去实现更多的自定义扩展,可以参考“开发新的扩展” 。
选择报告类型¶
查看当前支持的所有报告类型,可以通过下面代码打印:
from testbase.report import report_types
print(report_types.keys())
根据支持的类型,先获取到对应报告类型的class,然后实例化一个报告对象传递给TestRunner,用于存储执行结果:
from testbase.report import report_types
report_type = report_types['xml']
report = report_type() # 根据实际类型,可以在构造时传入对应的参数
自定义测试报告需要实现接口类“testbase.report.ITestReport
”和“testbase.report.ITestResultFactory
”。
由于测试结果本身由测试报告类生成和管理,用户也可以同时自定义新的测试结果类型,基于“testbase.testresult.TestResultBase
”实现。
更多测试报告相关的内容,请参考接口文档《testbase.report Package》。
选择资源管理后端类型¶
查看当前支持的所有资源管理后端类型,可以通过下面代码打印:
from testbase.resource import resmgr_backend_types
print(resmgr_backend_types.keys())
根据支持的类型,先获取到对应资源管理后端类型的class,然后实例化一个对象传递给TestRunner,用于管理资源:
from testbase.resource import resmgr_backend_types
resmgr_backend_type = resmgr_backend_types["local"]
resmgr_backend = resmgr_backend_type() # 根据实际类型,可以在构造时传入对应的参数
资源管理是提高测试效率和保障测试通过率的重要部分,框架支持用户自己扩展资源管理后端,可以参考“扩展资源管理后端”。
更多关于资源管理相关的内容,请参考文档《测试资源管理》或接口文档“testbase.resource Package”。
选择执行器类型¶
查看当前支持的所有资源管理后端类型,可以通过下面代码打印:
from testbase.runner import runner_types
print(runner_types.keys())
根据支持的类型,先获取到对应TestRunner类型的class,然后实例化一个对象用于执行测试用例。
结合上面的测试报告类型和资源管理后端类型的选择,我们可以如下实现一个输出xml报告的执行逻辑:
from testbase.resource import resmgr_backend_types
from testbase.report import report_types
from testbase.runner import runner_types
resmgr_backend = resmgr_backend_types["local"]()
report = report_types["xml"]()
runner_type = runner_types["multithread"]
runner = runner_type(report, retries=1, resmgr_backend=resmgr_backend) # 根据实际类型,可以在构造时传入对应的参数
runner.run("zoo.test")
自定义测试执行器可以以“testbase.runner.BaseTestRunner
”为基类。
更多TestRunner相关的内容,请参考接口文档《testbase.runner Package》。
指定测试用例集¶
TestRunner指定测试用例的方法也很灵活,可以是字符串:
runner.run("zootest.cat.feed")
如果存在多个用例集,可以用空格间隔:
runner.run("zootest.cat.feed zootest.dog")
也可以使用列表:
runner.run(["zootest.cat.feed", "zootest.dog"])
也可以直接指定“ testbase.testcase.TestCase
”对象列表:
from testbase.loader import TestLoader
tests = TestLoader().load("zootest")
runner.run(test)
使用“testbase.runner.TestCaseSettings
”可以充分利用框架支持的所有特性来过滤用例,
包括name、owner、priority、status和tag,例如:
from testbase.runner import TestCaseSettings
from testbase.testcase import TestCase
runner.run(TestCaseSettings(
names=["zootest"],
status=[TestCase.EnumStatus.Ready]
))
TestRunner也支持执行“testbase.plan.TestPlan
”对象,详情请参考“测试计划”或接口文档“testbase.runner Package”。
测试计划¶
在“执行测试”中介绍了一种批量执行测试用例和生成对应测试报告的方法,但是在实际测试执行中,还需要一些前置和后置的动作,以及对测试资源(帐号、设备等)进行初始化和清理,而“测试计划”就是用于解决这个问题。
定义测试计划¶
测试计划都以“testbase.plan.TestPlan
”为基类:
from testbase.plan import TestPlan
class AndroidAppTestPlan(TestPlan):
"""Android App test plan
"""
tests = "adtest"
test_target_args = "http://package.com/xx.apk"
def get_test_target(self):
"""获取被测对象详情
"""
return {"apk": self.test_target_args",
"version": tool_get_apk_ver(self.test_target_args)}
def test_setup(self, report):
"""全局初始化
"""
install_tools("adb")
def resource_setup(self, report, restype, resource):
"""测试资源初始化
"""
if res_type == "android":
adb_install(resource["serialno"], self.test_target_args)
if __name__ == '__main__':
AndroidAppTestPlan().debug_run()
注解
TestPlan不允许子类重载__init__方法,否则会导致对象初始化失败。
上面的代码定义了一个测试计划,包括两个必要的类成员变量:
- tests:即要执行的测试用例,接受多种类型参数,可以参考“TestRunnerRunParam”
- test_target: 被测对象的参数,由用户自定义,可以是任意的Python类型,一般来说主要是字符串等
这里实现了两个接口
testbase.plan.TestPlan.get_test_target()
:用于解析被测对象参数,并返回对应的被测对象的Key-Value信息,具体的KV结构完全由用户自定义,这个方法返回的结果会提供给测试报告进行记录testbase.plan.TestPlan.test_setup()
:全局初始化,会在测试执行之前处理一次testbase.plan.TestPlan.resource_setup()
:资源初始化,针对每个资源都会有一次操作。这里大部分的资源由资源管理系统提供,资源的注册和新增可以通过资源管理的接口实现,详情可以参考“注册非文件资源类型”;但有一种特殊的资源类型“node”会由测试执行器定义,node类型的资源表示的是当前执行测试用例的主机,因此,如果需要对当前执行测试的主机环境进行预处理,可以针对node类型的资源进行处理即可。
和初始化的接口对应的,TestPlan也同时提供的清理接口:
testbase.plan.TestPlan.test_teardown()
:全局清理testbase.plan.TestPlan.resource_teardown()
:资源清理
如果需要也可以重载以上两个方法。
调试测试计划¶
和测试用例类似,测试计划也提供了 testbase.plan.TestPlan.debug_run()
的方法用于调试执行。像上面的例子,在__main__分支下调用debug_run后,只要直接执行当前的脚本就可以实现调试。
默认情况下,执行测试计划会执行全部用例,且使用 testbase.report.StreamTestReport
类型的报告和 testbase.resource.LocalResourceManagerBackend
类型的后端,如果用户需要指定对应的后端,可以通过参数传递给debug_run方法:
if __name__ == '__main__':
from testbase.report import XMLReport
from testbase.resource import LocalResourceManagerBackend
AndroidAppTestPlan().debug_run(
report=XMLReport(),
resmgr_backend=LocalResourceManagerBackend())
测试计划存放的位置¶
测试计划的存放位置框架没有强制的要求,建议一般是存放在“testplan”名字后缀的Python包或模块中,比如下面的项目代码结构:
/footestproj/
footest/
footestplan/
func.py <----功能测试计划
perf.py <----性能测试计划
foolib/
exlib/
resources/
settings.py
manage.py
执行测试计划¶
正式执行测试计划有两种方式,一种是通过QTAF提供的命令行工具,一种是直接调用QTAF的接口
命令行接口¶
qta-manage接口和每个项目的manage.py都有提供“runplan”命令用于执行一个测试计划。
如果通过qta-manage调用,可以针对已经打包(参考“测试项目打包”)的项目中的测试计划进行执行:
$ qta-manage runplan footest-1.0.0.tar.gz footestplan.FooTestPlan
如果通过manage.py调用:
$ manage.py runplan footestplan.FooTestPlan
此外,qta-manage和manage.py的runplan和runtest类似,都提供选择测试类型执行器、测试报告、资源管理类型的参数,详情可以参考“使用包”。
配置测试项目¶
本节主要介绍如何修改测试项目的配置文件 settings.py来修改测试框架的行为。如果需要查询QTA框架的全部配置项,请参考《配置项说明文档》。
配置语法¶
测试项目的配置文件是一个python模块,所以配置项都是python模块的模块变量,如下所示:
DEBUG = True
RUNNER_THREAD_COUNT = 5
LOCAL_REPORT = 'console'
由于使用的是python模块表示,因此需要符合以下要求:
- 需要符合python语法要求
除此之外,对于配置项还需要符合以下要求:
- 配置变量名必须使用大写
- 配置变量名不可以双下划线开头
比如下面的变量都是非法的:
lower_test = 34
__CONFIG = "XXX"
配置文件¶
QTA配置文件分为三种:
- 用户配置文件
- 依赖Egg包的配置文件
- Testbase配置文件(即qtaf_settings模块)
注解
注意依赖Egg包的配置文件只有通过“manage.py installlib”方式安装到测试项目中,其配置文件才会被加载,具体的依赖egg,可以参考exlib下的installed_libs.txt
用户配置文件存放在测试项目的顶层位置;而QTAF配置文件打包在QTAF的egg包中,在QTAF egg包的顶层位置上;如下:
test_proj/
qt4a/
exlib/
qtaf.egg/
testbase/
tuia/
pyqq/
qtaf_settings.py # Testbase配置
qt4i.egg/
qt4i/settings.py # 依赖Egg包的配置文件
mqlib/
mqtest/
settings.py # 用户配置
当两个配置文件中的配置项存在冲突时,按照以下优先级从高到低处理:
- 用户配置文件
- 依赖Egg包的配置文件
- Testbase配置文件
也就是说,用户配置文件可以重载QTAF配置中的默认配置。
配置文件定位¶
上面提到的三种配置文件,对于存在整个工程的情况来说,就可以直接使用,不需要额外处理。 如果想要独立使用qtaf或其他qta的egg模块,可以采用定义环境变量的方式告诉qtaf配置文件的位置:
QTAF_EXLIB_PATH: 指定qta相关egg包存放的路径,qtaf、qt4s、qt4a等egg都会去这里查找,并加载配置
QTAF_INSTALLED_LIBS: 指定已安装并计划使用的第三方模块(即qtaf除外的),多个模块间用分号隔开,例如:qt4s;qt4a;qt4i
QTAF_SETTINGS_MODULE: 指定用户自定义的配置模块,python在运行时可以找到的模块,支持多级路径,例如:myproject.settings_20160705
警告
特别注意,如果环境变量存在,仅仅使用环境变量指定的内容,例如存在QTAF_INSTALLED_LIBS环境变量,就不会使用exlib目录下的installed_libs.txt中的内容了
使用测试配置¶
配置使用的接口统一使用conf接口,如下:
from testbase.conf import settings
if settings.DEBUG:
print 'debug mode'
else:
print 'release mode'
也可以使用get接口查询配置,比如:
from testbase.conf import settings
my_conf = settings.get('MY_SETTING', None)
警告
settings.py和qtaf_settings.py也是可以直接import使用的,但是不建议这样做,如果这样使用,可能会遇到非预期的结果。
注意settings配置不允许动态修改配置的值,如:
settings.DEBUG = False
会导致异常:
Traceback (most recent call last):
File "D:\workspace\qtaftest\test.py", line 17, in <module>
settings.DEBUG = 9
File "build\bdist.win32\egg\testbase\conf.py", line 85, in __setattr__
RuntimeError: 尝试动态修改配置项"DEBUG"
自定义settings所在的文件¶
QTA默认是通过加载Python模块`settings`来读取所有配置,用户可以通过设置环境变量`QTAF_SETTINGS_MODULE`来指定配置项所在的模块名。
比如在测试项目中顶层目录中创建多个配置文件:
用户配置文件存放在测试项目的顶层位置;而QTAF配置文件打包在QTAF的egg包中,在QTAF egg包的顶层位置上;如下:
test_proj/
qt4a/
exlib/
mqlib/
mqtest/
settings/
__init__.py
prod.py #正式环境
test.py #测试环境
比如需要使用正式环境的配置:
$ QTAF_SETTINGS_MODULE=settings.prod python manage.py shell
比如需要使用测试环境的配置:
$ QTAF_SETTINGS_MODULE=settings.test python manage.py shell
使用SettingsMixin¶
SettingsMixin是一个混合类,用于方便地跟用户定义的类进行复合,在定义配置项的时候, 将定义放到lib层,而不是孤立地放在settings.py或配置模块中,再人工进行关联。
定义配置项¶
一个简单的使用例子如下:
from qt4s.service import Channel
from qt4s.conn2 import HttpConn
from testbase.conf import SettingsMixin
class MyChannel(Channel, SettingsMixin):
"""define a pseudo channel
"""
class Settings(object):
MYCHANNEL_URL = "http://www.xxxx.com"
def __init__(self):
self._conn = HttpConn()
def get(self, uri, params):
return self._conn.get(self.settings.MYCHANNEL_URL + uri, params)
MyChannel多重继承了Channel和SettingsMixin,SettingsMixin要求类的内部定义一个Settings类, 这个类定义配置项的规则如下:
- 配置项必须以当前类的名字大写+下划线开头,例如这里的"MYCHANNEL_";
- 配置项的每个字母都必须大写;
- 访问配置项,使用self.settings访问,例如self.settings.MYCHANNEL_URL
重载配置项¶
上面,我们已经知道如何在lib层定义配置项,当需要重载某个配置项的值的时候,在全局配置项里面定义该配置就可以了, 即testbase.conf.settings包含该配置项。lib层的定义跟上面的定义保持一致,而settings.py配置如下
settings.py:
MYCHANNEL_URL = "http://www.oooo.com"
那么在访问self.settings.MYCHANNEL_URL的时候,会优先获取testbase.conf.settings中的配置项。
测试资源管理¶
在测试执行过程中,需要使用或依赖一些资源(文件、设备等),为此,框架提供了统一的测试资源的管理和使用接口,以及新增测试资源的扩展接口。
- 目前的测试资源分为两类:
- 文件资源
- 非文件资源
文件资源的存储¶
若要使用文件资源管理器去测试文件资源,需要一定的规则来存放测试文件资源。
一般测试文件资源存放方式¶
每个QTA测试项目的目录中新增一个目录“resources”(新创建项目自带,老项目可以手动创建):
/footestproj/
footest/
foolib/
exlib/
resources/
settings.py
manage.py
用户将需要使用的文件资源都存放在此目录下,文件资源的组织自由,可以也推荐用户按需要创建多级目录,比如:
/footestproj/
footest/
foolib/
exlib/
resources/
test.txt
video/
foo.mp4
audio/
foo.mp3
settings.py
manage.py
文件定位搜索逻辑¶
有部分QTA项目涉及到外链代码有资源文件访问的情况,所以资源文件读取会搜索当前工程目录所有“resources”目录去寻找资源:
/footestproj/
footest/
foolib/
exlib/
resources/
test.txt
video/
foo.mp4
audio/
foo.mp3
mqlib/
resources/
qq.jpeg
settings.py
manage.py
不过,这种情况下要保证所有的“resources”目录下不能同名(相对路径相同)的文件资源。
外链文件资源¶
对于比较大的文件资源,SVN等代码管理系统的限制,导致没法存放在代码库中的时候,可以通过软链接的方式存放,具体的方法是在resources目录中创建一个文本文件,后缀名为“.lnk”。比如下面的的例子:
/footestproj/
footest/
foolib/
exlib/
resources/
test.txt
video/
bigfile.mp4.lnk #软链接文件
foo.mp4
audio/
foo.mp3
settings.py
manage.py
bingfile.mp4.lnk是一个文本文件,其内容为文件正在的路径,比如可以是一个HTTP下载路径
也可以是一个本地绝对路径:
/data/foo/xx/bigfile.mp4
资源路径格式¶
由于操作系统的差异,路径的分隔符可能是“/”或“”,以上的接口不区分操作系统,且两种分隔符都同时支持。 比如下面的两个路径是等价的,在Windows/Mac/Linux上都能同时使用:
video/foo.mp4
video\foo.mp4
文件资源的使用接口¶
- 目前提供两个操作文件资源的方法:
- get_file:获取指定文件对象,传入参数为上文描述规则的相对路径,返回的是文件对象的绝对路径。
- list_dir:列举指定目录下文件对象(包括文件夹),传入参数为上文描述规则的相对路径,返回的是一个包含该路径下所有文件对象的绝对路径的list。
用户如果需要在测试用例中使用特定的文件资源的时候,可以通过访问TestCase基类提供的方法:
from testbase.testcase import TestCase
class HelloTest(TestCase):
'''文件资源测试用例
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
#---------------------------
self.start_step("测试文件资源管理接口")
#---------------------------
paths = self.test_resources.list_dir("video")
self.log_info(paths)
mp4_filepath = self.test_resources.get_file("video/foo.mp4")
self.assert_equal("文件存在", os.path.isfile(mp4_filepath), True)
bigfile_path = self.test_resource.get_file("video/bigfile.mp4.lnk")
self.assert_equal("文件存在", os.path.isfile(bigfile_path ), True)
在lib层中可以直接使用文件管理的接口来实现相应的逻辑,如下:
from testbase import resource
def get_test_video_path():
return resource.get_file("video/foo.mp4")
非文件资源¶
非文件资源指除了文件形态外的其他资源类型,比如执行用例的设备、使用的终端设备、测试帐号等都属于此类。非文件资源管理主要用于解决可能导致的资源使用冲突,对于并行执行测试用例的场景尤其必要。
对于QTA来说,非文件资源和文件资源的主要区别是:
- 非文件资源存储形态多样化,可以是在本地的CSV文件、数据库或远程的服务器的数据库等形态
- 非文件资源类型是多样且可扩展的,所以在使用之前,需要先注册给QTA框架
注册非文件资源类型¶
注册资源类型需要通过“testbase.resource.LocalResourceManagerBackend
”接口注册一个以“testbase.resource.LocalResourceHandler
”为基类的Handler。
比如我们用一个本地的CSV文件来管理测试资源:
import csv
from testbase.testcase import TestCase
from testbase.resource import LocalResourceManagerBackend, LocalCSVResourceHandler
LocalResourceManagerBackend.register_resource_type(
"account",
LocalCSVResourceHandler("/path/to/account.csv"))
如果需要,也可以通过以“testbase.resource.LocalResourceHandler
”为基类自定义一个资源类型,比如对于Android手机设备,设备资源是通过ADB工具动态查询得到的,则可以这样实现:
from testbase.resource import LocalResourceManagerBackend, LocalResourceHandler
class AndroidResourceHandler(LocalResourceHandler):
def iter_resource(self, res_type, res_group=None, condition=None):
for it in ADBClient().list_device():
yield {"id": it["serialno"], "host":"localhost", "serialno":it["serialno"]}
非文件资源的使用¶
非文件测试资源一般只允许在测试用例,和文件资源一样,也通过test_resources接口(类型为:“testbase.resource.Session
”)使用:
from testbase.testcase import TestCase
from testbase.resource import LocalResourceManagerBackend, LocalCSVResourceHandler
LocalResourceManagerBackend.register_resource_type(
"account",
LocalCSVResourceHandler("/path/to/account.csv"))
class HelloTest(TestCase):
'''非文件资源测试用例
'''
owner = "foo"
status = TestCase.EnumStatus.Ready
priority = TestCase.EnumPriority.Normal
timeout = 1
def run_test(self):
acc = self.test_resources.acquire_resource("account")
app = FooApp()
app.login(acc["username"], acc["password"])
acquire_resource如果申请成功会返回一个资源的dict,其中除了必要的id、res_group(分组)属性外,还有其他资源自定义的属性。
- acquire_resource接口还提供两个可选参数:
- res_group: 指定资源的分组
- condition: 指定匹配的资源的属性的字典
比如可以这样使用:
class HelloTest(TestCase):
'''非文件资源测试用例
'''
...
def run_test(self):
acc = self.test_resources.acquire_resource("account", res_group="foo", condition={"vip": True})
...
- 如果申请资源失败,则会导致异常。有两种情况会导致申请资源失败:
- 指定条件的资源不存在
- 指定条件的资源存在,但是目前都被占用。对于这种情况,会产生一个RESNOTREADY级别的日志
一般来说资源的使用不需要显式释放,测试用例执行完成或超时时,测试框架会负责回收。如果用户需要手动释放资源,则可以通过release_resource接口:
class HelloTest(TestCase):
'''非文件资源测试用例
'''
...
def run_test(self):
acc = self.test_resources.acquire_resource("account")
self.test_resource.release_resource("account", acc["id"])
如果需要的话,在lib层中可以直接使用非文件管理的接口来实现相应的逻辑,如下:
from testbase import resource
def get_special_resource():
return resource.acquire_resource("account", res_group="special")
测试项目打包¶
QTA内置测试项目打包的功能,方便将测试项目打包并发布给支持QTA测试执行的执行系统
执行打包¶
调用测试项目的manage.py:
$ python manage.py dist --version 1.0.0
执行成功后可以看到生成文件在dist目录下:
dist/
foo-1.0.0.tar.gz
使用包¶
对于生成的包,QTA内置了执行测试的工具,可以通过调用qta-manage命令来执行测试:
$ qta-manage runtest foo-1.0.0.tar.gz footest
qta-manage run命令提供丰富的控制参数来控制测试用例的执行范围。比如,只执行特定状态的用例:
$ qta-manage runtest foo-1.0.0.tar.gz footest --status Ready --status BVT
除了状态外,用例优先级、负责人等都能作为过滤的选项,也能通过--exclude-name来排除特定的包或模块的用例集:
$ qta-manage runtest foo-1.0.0.tar.gz footest --exclude-name footest.hello
QTA打包生成的包是Python的sdist标准格式,而qta-manage runtest命令是通过生成一个virtualenv来执行测试的Python代码,如果需要,用户也能控制使用的virtualenv:
$ qta-manage runtest foo-1.0.0.tar.gz footest --venv /path/to/your/venv
qta-manage run命令,也能控制使用的测试执行器、测试报告类型和测试资源管理后端类型:
$ qta-manage runtest foo-1.0.0.tar.gz footest --runner-type multithread --report-type json
上面的命令行就指定使用多线程执行,并生成JSON格式的报告;更多的可选的执行器、报告类型可以通过qta-manage run的--help参数查询。 在指定特定类型的runner和report后,也能传递参数给特定类型的runner和report,例如:
$ qta-manage runtest foo-1.0.0.tar.gz footest --runner-type multithread --runner-args "--concurrent 10"
比如上面的命令就指定多线程执行时使用10个线程。具体的runner和report类型有哪些可选参数,可以通过这样获取:
$ qta-manage foo-1.0.0.tar.gz runtest --runner-args-help multithread
usage: qta-manage [-h] [--retries RETRIES] [--concurrent CONCURRENT]
optional arguments:
-h, --help show this help message and exit
--retries RETRIES retry count while test case failed
--concurrent CONCURRENT
number of concurrent thread
同理,测试报告也能通过“--report-args-help”查询。
开发新的扩展¶
QTAF的扩展允许用户扩展QTAF命令行工具的功能。通过实现扩展,用户能定制化测试执行和资源管理的方式,也能定制自定义的测试报告的格式,方便第三方的系统或平台开发对QTA测试用例的执行的支持。
扩展点¶
目前支持扩展的功能有:
- qta-manage
- runtest命令
- runplan命令
- 每个项目的manage.py
- runtest命令
- runplan命令
以上的命令都支持用户自定义测试执行器(TestRunner)、测试报告(TestReport)和测试资源管理后端(TestResourceManagerBackend)
实现扩展¶
QTAF的扩展使用Python setuptools提供的 Entry point机制。QTAF定义了三个Entry points:
- qtaf.runner:测试执行器类型扩展点,对应接口 “
testbase.runner.BaseTestRunner
”,更多请参考“CustomTestRunner”- qtaf.report:测试报告类型扩展点,对应接口 “
testbase.report.ITestReport
”,更多请参考“CustomTestReport”- qtaf.resmgr_backend:资源管理后端扩展点,对应接口 “
testbase.resource.IResourceManagerBackend
”,更多请参考“扩展资源管理后端”
下面以测试执行器为例子,定义一个名字为foo的测试执行器:
# foo.py
import argparse
from testbase.runner import BaseTestRunner
class FooTestRunner(BaseTestRunner):
def run_all_tests(self, tests ):
tests.sort(lambda x,y: cmp(x.owner, y.owner)) #按用户排序执行
for test in tests:
self.run_test(test)
@classmethod
def get_parser(cls):
'''获取命令行参数解析器(如果实现)
:returns: 解析器对象
:rtype: argparse.ArgumentParser
'''
return argparse.ArgumentParser()
@classmethod
def parse_args(cls, args_string, report, resmgr_backend):
'''通过命令行参数构造对象
:returns: 测试报告
:rtype: cls
'''
return cls(report, resmgr_backend)
以上就实现了一个定制化的测试执行器,测试用例会按照用户名字排序执行。代码实现后,还需要打包和声明Entry point:
# setup.py
from setuptools import setup, find_packages
setup(
version="1.0.0",
name="qtaf-ext-foo",
py_modules=["foo"],
include_package_data=True,
package_data={'':['*.txt', '*.TXT'], },
entry_points={
'qtaf.runner': ['foo = foo:FooTestRuner'],
},
)
然后是打包和安装,如果是在开发调试,可以这样执行:
$ python setup.py develop
如果是正式打包和安装:
$ python setup.py install
如果安装成功,在执行qta-manage run是可以指定此类型的runner:
$ qta-manage run foo-1.0.0.tar.gz run footest --runner-type foo
接口文档¶
接口文档¶
testbase.conf
Package¶
testbase.context
Package¶
testbase.datadrive
Package¶
testbase.exlib
Package¶
testbase.loader
Package¶
testbase.logger
Package¶
testbase.management
Package¶
testbase.project
Package¶
testbase.report
Package¶
testbase.resource
Package¶
testbase.runner
Package¶
testbase.serialization
Package¶
testbase.testcase
Package¶
testbase.testresult
Package¶
testbase.dist
Package¶
testbase.plan
Package¶
testbase.util
Package¶
配置项说明文档¶
DATA_SROUCE¶
如果是字符串,则用于指定全局数据驱动的数据文件路径,使用相对路径,“/”标识测试项目的根目录;指定的数据文件使用Python代码格式,其中需要定义一个列表或字典兼容类型的“DATASET”变量,表示测试数据驱动的源数据。
如果是列表或字典兼容类型,则表示数据驱动的数据源。
适用5.0.0或以上版本。
PROJECT_ROOT¶
仅标准模式下配置文件中的改设置才有效,用于指定项目的根目录,此目录会加到python path中。在独立模式下PROJECT_ROOT的指会自动推导得到,因此不能修改和设置。
适用5.0.97或以上版本。
INSTALLED_APPS¶
仅标准模式下配置文件中的改设置才有效,用于指定已安装的QTAF库。在独立模式下INSTALLED_APPS的值通过读取exlib/installed_libs.txt得到,,因此不能修改和设置。
适用5.0.97或以上版本。