QTA Testbase文档

Testbase是所有QTA测试项目的基础,主要提供测试用例管理和执行、测试结果和报告、测试项目管理配置等功能。

使用文档

使用前准备

Python版本依赖

目前QTAF在Python 2.7版本上测试,推荐使用Python2.7版本,如果是Windows系统,推荐使用ActivePython打包的版本。

最新的Mac OSX内置的Python已经为2.7版本,一般无需配置。

通过pip工具安装

安装方法如下:

pip install qtaf

警告

如果是mac os的用户,在使用pip安装的时候最好指定\--user选项,否则安装的script脚本可能无法使用。

通过Git拉取最新代码

操作方式如下:

git clone https://github.com/tencent/qtaf

创建和修改测试项目

对于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")

那么实际的得到的结果输出如下:

_images/assert_fail.png

可以看到,堆栈中对应的代码行就是我们书写的那条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()

这个用例和一般用例的区别是:

直接执行以上代码,输出为:

============================================================
测试用例: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是按照下面的流程处理的:

  1. 获取尝试测试用例对应的case_runner静态变量,如果不存在,则设置case_runner为一个“testbase.testcase.TestCaseRunner”实例
  2. 使用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、html,默认是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或指定文件路径。命令行参数如下:

  • \--title,测试报告的标题;
  • \--output/-o,输出文件名称,会将对应文件输出到当前工作目录,必填参数。

empty类型,将不输出报告内容。无命令行参数。

stream类型,将报告内容输出到stdout,与调试用例时debug_run输出的信息一致。命令行参数如下:

  • \--no-output-result,指定后,用例执行的中间内容将不会输出到报告;
  • \--no-summary,指定后,将不输出用例执行统计结果。

html类型,会生成js和html文件,使用浏览器打开工作目录下的qta-report.html即可查看,命令行参数如下:

  • \--title,测试报告的标题;

例如:

$ 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
$ python manage.py runtest zoo --report-type html -w test_result --report-args "--title zootest"
指定资源管理后端

可以通过\--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核数作为并发数。

multiprocess类型,使用多进程来并发执行用例。命令行参数如下:

  • \--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.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类似,都提供选择测试类型执行器、测试报告、资源管理类型的参数,详情可以参考“使用包”。

类接口

testbase.runner.TestRunner.run”也支持传入“testbase.plan.TestPlan”对象:

from testbase.runner import TestRunner
from testbase.report import StreamTestReport
from footestplan import FooTestPlan
TestRunner(StreamTestReport()).run(FooTestPlan())

TestRunner其他的用法和执行用例的方式一致,详情请参考“执行测试”。

配置测试项目

本节主要介绍如何修改测试项目的配置文件 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"

增加配置项

QTA对配置项的新增没有严格的限制,但是为避免冲突,最好按照以下的原则:

  • 测试项目自定义的配置,增加一个统一的前缀,比如QQ的测试项目增加前缀“QQ_
  • QTA相关组件的配置项目,除了统一增加前缀外,还需要更新到《配置项说明文档

自定义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

重载配置项

重载配置项,分为两种情况

派生类重载

如果某个SettingsMixin类被继承,那么子类可以访问父类所拥有的配置项,并且可以重载父类的配置项,但是这里重载的方式比较特殊。

因为SettingsMixin要求当前类下必须定义一个嵌套Settings类,并且配置项必须以类名加下划线开头,因此,子类要重载父类的配置项, 通过定义相同后缀的配置项来实现,如下面的DUMMY_A和DUMMYCHILD_A,它们的后缀名都是"A",这样才会生效。

一个具体的例子如下:

from testbase.conf import SettingsMixin

class Dummy(SettingsMixin):
    class Settings(object):
        DUMMY_A = 0

    def print_a(self):
        print("DUMMY_A=%s" % self.settings.DUMMY_A)

class DummyChild(Dummy):
    class Settings(object):
        DUMMYCHILD_A = 2

dummy = Dummy()
assert dummy.settings.DUMMY_A == 0
dummy.print_a()
# DUMMY_A = 0

child = DummyChild()
assert child.settings.DUMMY_A == 2
assert child.settings.DUMMYCHILD_A == 2
child.print_a()
# DUMMY_A = 2

如上,我们看到,在覆盖掉父类的配置项后,在父类的方法中访问的配置项也一样会被重载,这样可以复用父类的一些配置项,并根据需要进行重载。

全局配置项重载

SettingsMixin定义的配置项还可以被全局配置项重载,并且全局配置项的优先级最高。我们仍然用上面的Dummy和DummyChild来说明问题。

settings.py:

DUMMYCHILD_A = 3

xxxxcase.py:

dummy = Dummy()
assert dummy.settings.DUMMY_A == 0
dummy.print_a()
# DUMMY_A = 0

child = DummyChild()
assert child.settings.DUMMY_A == 3
assert child.settings.DUMMYCHILD_A == 3
child.print_a()
# DUMMY_A = 3

可以看到,即使子类重载了DUMMY_A的值为2,但是仍然可以在settings.py中已更高的优先级将其修改。

警告

框架在SettingsMixin定义的某个配置项被子类重载后,是不允许再在settings.py中去重载该配置项的, 即:如果我们在settings.py中添加DUMMY_A = 5,框架会提示错误,要求用户去重载DUMMYCHILD_A,而不是DUMMY_A。 这样可以防止使用配置项在派生类之间冲突,并且简化配置项的设置。

测试资源管理

在测试执行过程中,需要使用或依赖一些资源(文件、设备等),为此,框架提供了统一的测试资源的管理和使用接口,以及新增测试资源的扩展接口。

目前的测试资源分为两类:
  • 文件资源
  • 非文件资源

文件资源的存储

若要使用文件资源管理器去测试文件资源,需要一定的规则来存放测试文件资源。

一般测试文件资源存放方式

每个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")

扩展资源管理后端

上面的资源管理都是基于内置的“testbase.resource.LocalResourceManagerBackend”资源管理后端,一般来说能满足本地单机执行测试的要求,但如果对于支持QTA自动化测试的平台,在执行多机分布式执行测试的情况时,则可能需要扩展对应的资源管理后端。

用户如果要实现测试资源管理后端,需要实现接口类“testbase.resource.IResourceManagerBackend

资源管理后端也可以以QTAF的扩展的形式实现,更多细节请参考“开发新的扩展”。

测试项目打包

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:

下面以测试执行器为例子,定义一个名字为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

关于扩展包命名的规范

请按照包格式:

qtaf-ext-<your name>

接口文档

接口文档

testbase.conf Package

配置接口

一、配置格式 配置文件名默认为settings.py,可以通过环境变量指定用户配置文件的路径 qtaf相关配置的环境变量为: QTAF_EXLIB_PATH: 指定exlib的路径,exlib下放置对应的qtaf、qt4s、installed_libs.txt等文件 QTAF_INSTALLED_LIBS: 指定已经安装的库的列表,以分号分隔,例如qt4s,用来代替不使用installed_libs.txt的场景 QTAF_SETTINGS_MODULE: 指定用户自定义的配置文件的路径,最后加载,会覆盖已经存在的配置

配置变量名必须符合pyton变量名规范,且统一使用大写,且不可以以"__"开头

如: CONFIG_OPTION = True DEBUG = True RUNNER_THREAD_CNT = 5 BANNER = "hello"

二、使用示例: from testbase.conf import settings print(settings.CONFIG_OPTION)

注意 settings的值都是只读的,不可以修改,如果尝试修改会导致异常

三、配置优先级 配置存在2个优先级,当存在名字冲突时,使用高优先级的配置的值。优先级自低到高分别为: 1、QTAF配置 固定为:test_proj/exlib/qtaf.egg/qtaf_settings.py 2、lib配置 已经配置在test_proj/exlib/installed_libs.txt的包中的settings模块 3、用户自定义配置 固定为:test_proj/settings.py

class testbase.conf.SettingsMixin

基类:object

a mixin class coordinate with qtaf settings

testbase.context Package

测试用例执行时上下文

testbase.context.current_testcase()

当前正在执行的用例

返回:TestCase
testbase.context.current_testresult()

当前正在执行的用例对应的测试结果

返回:TestResult

testbase.datadrive Package

数据驱动模块

使用介绍: 1、在TestCase类前面加入@DataDrive装饰器,参数为测试数据列表或者字典:

@datadrive.DataDrive([1,2,3])
class Test(tc.TestCase):
    def runTest(self):
        pass

@datadrive.DataDrive(
    {
    'TEST1':1,
    'TEST2':2,
    'TEST3':3,
    }
    )
class Test(tc.TestCase):
    def runTest(self):
        pass

在测试报告中展示用例名字时,若参数为列表的,则在用例后面增加一个索引序号,若参数为字典的,则在用例后面增加上key的名字:

  • 列表显示为:

    Test#1
    Test#2
    Test#3
    
  • 字典显示为:

    Test#TEST1
    Test#TEST2
    Test#TEST3
    

2、可以在runTest里通过self.casedata使用测试数据:

def runTest(self):
    print(self.casedata)

3、运行及调试方法和原来一样:

MyTest().run()
    

完整的例子如下::

# -*- coding: utf-8 -*-

import testbase.testcase as tc
import testbase.datadrive as datadrive

@datadrive.DataDrive([1,2,3])
class MyTest(tc.TestCase):
    """test
    """
    
    owner = 'foo'
    priority = tc.TestCase.EnumPriority.High
    status = tc.TestCase.EnumStatus.Ready
    timeout = 5       
    
    def runTest(self):
        print 'runTest, casedata:', self.casedata
          
        
if __name__ == '__main__':
    MyTest().run()
class testbase.datadrive.DataDrive(case_datas)

基类:object

数据驱动类修饰器,标识一个测试用例类使用数据驱动

testbase.datadrive.get_datadrive(obj)

获取对应用例的数据驱动

参数:obj (TestCase/type) -- 测试用例或测试用例类

:returns DataDrive

testbase.datadrive.is_datadrive(obj)

是否为数据驱动用例

参数:obj (TestCase/type) -- 测试用例或测试用例类

:returns boolean

testbase.datadrive.load_datadrive_tests(cls, name=None)

加载对应数据驱动测试用例类的数据驱动用例

testbase.exlib Package

扩展库管理(仅独立模式使用)

class testbase.exlib.ExLibManager(proj_root)

基类:object

扩展库管理器

install(egg_path)

安装

list_names()

获取全部的扩展包的名字

testbase.loader Package

class testbase.loader.TestDataLoader

基类:object

测试数据加载器

load()

从数据源加载测试数据 :returns list - 测试数据集

class testbase.loader.TestLoader(filter_func=None)

基类:object

测试用例加载器

get_filtered_tests()

返回最后一次load调用时被过滤掉的测试用例

get_filtered_tests_with_reason()

返回最后一次load调用时被过滤掉的测试用例和过滤原因

get_last_errors()

返回最后一次load调用时加载失败的全部模块和对应错误信息

Returns dict:模块和对应的错误信息
load(testname)

通过名字加载测试用例

参数:name (string/list) -- 用例或用例名称

:returns list - 测试用例对象列表

testbase.logger Package

log模块

class testbase.logger.TestResultBridge(level=0)

基类:logging.Handler

中转log信息到TestResult

emit(log_record)

Log Handle 必须实现此函数

testbase.logger.addHandler(hdlr)

Add the specified handler to this logger.

testbase.logger.debug(msg, *args, **kwargs)

Log a message with severity 'DEBUG' on the root logger.

testbase.logger.error(msg, *args, **kwargs)

Log a message with severity 'ERROR' on the root logger.

testbase.logger.exception(msg, *args)

Log a message with severity 'ERROR' on the root logger,with exception information.

testbase.logger.info(msg, *args, **kwargs)

Log a message with severity 'INFO' on the root logger.

testbase.logger.log(level, msg, *args, **kwargs)

Log 'msg % args' with the integer severity 'level' on the root logger.

testbase.logger.removeHandler(hdlr)

Remove the specified handler from this logger.

testbase.logger.warn(msg, *args, **kwargs)

Log a message with severity 'WARNING' on the root logger.

testbase.logger.warning(msg, *args, **kwargs)

Log a message with severity 'WARNING' on the root logger.

testbase.management Package

管理和辅助工具

class testbase.management.ArgumentParser(subcmd_classes)

基类:object

参数解析

get_subcommand(name)

获取子命令

parse_args(args)

解析参数

print_help()

打印帮助文档

class testbase.management.Command

基类:object

一个命令

execute(args)

执行过程

class testbase.management.CreateProject

基类:testbase.management.Command

创建测试项目

execute(args)

执行过程

class testbase.management.Distribute

基类:testbase.management.Command

Generate distribution packages

execute(args)

执行过程

class testbase.management.Help

基类:testbase.management.Command

帮助命令

execute(args)

执行过程

class testbase.management.InstallLib

基类:testbase.management.Command

安装扩展库

execute(args)

执行过程

class testbase.management.ManagementTools

基类:object

管理工具类入口

run()

执行入口

class testbase.management.ManagementToolsConsole(argparser)

基类:object

管理工具交互模式

class testbase.management.RunPlan

基类:testbase.management.Command

执行测试计划

execute(args)

执行过程

class testbase.management.RunPlanDistPackage

基类:testbase.management.Command

Run test plan in distribution package

execute(args)

执行过程

class testbase.management.RunScript

基类:testbase.management.Command

执行一个脚本

execute(args)

执行过程

class testbase.management.RunTest

基类:testbase.management.Command

批量执行测试用例

execute(args)

执行过程

class testbase.management.RunTestDistPackage

基类:testbase.management.Command

Run tests in distribution package

execute(args)

执行过程

class testbase.management.Shell

基类:testbase.management.Command

Python Shell

execute(args)

执行过程

class testbase.management.UpgradeProject

基类:testbase.management.Command

升级测试项目

execute(args)

执行过程

testbase.management.qta_manage_main()

qta-manage工具入口

testbase.project Package

测试项目接口

class testbase.project.EnumProjectMode

基类:object

测试项目运行模式

class testbase.project.Project(root)

基类:object

一个项目(废弃接口,存在是为了兼容)

testbase.project.create_project(dest_path, proj_name, mode)

创建项目

testbase.project.current_project()

获取当前项目(废弃接口,存在是为了兼容)

testbase.project.update_project_qtaf(proj_path, qtaf_egg_path)

升级项目的QTAF

testbase.report Package

测试报告

class testbase.report.EmptyTestReport(result_factory_func=None)

基类:testbase.report.ITestReport

不输出测试报告

classmethod get_parser()

获取命令行参数解析器(如果实现)

返回:解析器对象
返回类型:argparse.ArgumentParser
get_testresult_factory()

获取对应的TestResult工厂 :returns ITestResultFactory

log_test_result(testcase, testresult)

记录一个测试结果

参数:
  • testcase (TestCase) -- 测试用例
  • testresult (TestResult) -- 测试结果
classmethod parse_args(args_string)

通过命令行参数构造对象

返回:测试报告
返回类型:cls
passed

测试是否通过

class testbase.report.EmptyTestResultFactory(result_factory_func=None)

基类:testbase.report.ITestResultFactory

测试结果工厂

create(testcase)

创建TestResult对象 :param testcase: 测试用例 :type testcase: TestCase :return TestResult

dumps()

序列化 :return picklable object

loads(buf)

反序列化 :param buf: dumps返回的序列化后的数据 :type buf: object

class testbase.report.HtmlTestReport(title='xe8xb0x83xe8xafx95xe6xb5x8bxe8xafx95')

基类:testbase.report.JSONTestReportBase

html test report

end_report()

结束测试执行 :param passed: 测试是否通过 :type passed: boolean

classmethod get_parser()

获取命令行参数解析器(如果实现)

返回:解析器对象
返回类型:argparse.ArgumentParser
get_testresult_factory()

获取对应的TestResult工厂 :returns ITestResultFactory

classmethod parse_args(args_string)

通过命令行参数构造对象

返回:测试报告
返回类型:cls
class testbase.report.HtmlTestResultFactory

基类:testbase.report.ITestResultFactory

html test result factory

create(testcase)

创建TestResult对象 :param testcase: 测试用例 :type testcase: TestCase :return TestResult

class testbase.report.ITestReport

基类:object

测试报告接口

begin_report()

开始测试执行

critical(tag, msg, record=None)

记录一个CRITICAL日志 :param msg: 日志消息 :param tag: 日志标签 :param record: 日志记录信息 :type tag: string :type msg: string :type record: dict

debug(tag, msg, record=None)

记录一个DEBUG日志 :param msg: 日志消息 :param tag: 日志标签 :param record: 日志记录信息 :type tag: string :type msg: string :type record: dict

end_report()

结束测试执行

参数:passed (boolean) -- 测试是否通过
error(tag, msg, record=None)

记录一个ERROR日志 :param msg: 日志消息 :param tag: 日志标签 :param record: 日志记录信息 :type tag: string :type msg: string :type record: dict

classmethod get_parser()

获取命令行参数解析器(如果实现)

返回:解析器对象
返回类型:argparse.ArgumentParser
get_testresult_factory()

获取对应的TestResult工厂

:returns ITestResultFactory

info(tag, msg, record=None)

记录一个INFO日志 :param msg: 日志消息 :param tag: 日志标签 :param record: 日志记录信息 :type tag: string :type msg: string :type record: dict

log_filtered_test(loader, testcase, reason)

记录一个被过滤的测试用例

参数:
  • loader (TestLoader) -- 用例加载器
  • testcase (TestCase) -- 测试用例
  • reason (str) -- 过滤原因
log_load_error(loader, name, error)

记录一个加载失败的用例或用例集

参数:
  • loader (TestLoader) -- 用例加载器
  • name (str) -- 名称
  • error (str) -- 错误信息
log_loaded_tests(loader, testcases)

记录加载成功的用例

参数:
  • loader (TestLoader) -- 用例加载器
  • testcases (list) -- 测试用例列表
log_record(level, tag, msg, record)

增加一个记录

参数:
  • level (string) -- 日志级别
  • msg (string) -- 日志消息
  • tag (string) -- 日志标签
  • record (dict) -- 日志记录信息
log_resource(res_type, resource)

记录测试使用的资源

参数:
  • res_type (str) -- 资源类型
  • resource (dict) -- 资源详情
log_test_result(testcase, testresult)

记录一个测试结果

参数:
  • testcase (TestCase) -- 测试用例
  • testresult (TestResult) -- 测试结果
log_test_target(test_target)

记录被测对象

参数:test_target (any) -- 被测对象详情
classmethod parse_args(args_string)

通过命令行参数构造对象

返回:测试报告
返回类型:cls
warning(tag, msg, record=None)

记录一个WARN日志 :param msg: 日志消息 :param tag: 日志标签 :param record: 日志记录信息 :type tag: string :type msg: string :type record: dict

class testbase.report.ITestResultFactory

基类:object

TestResult工厂接口

create(testcase)

创建TestResult对象 :param testcase: 测试用例 :type testcase: TestCase :return TestResult

dumps()

序列化 :return picklable object

loads(buf)

反序列化 :param buf: dumps返回的序列化后的数据 :type buf: object

class testbase.report.JSONTestReport(fd, title='xe8xb0x83xe8xafx95xe6xb5x8bxe8xafx95')

基类:testbase.report.JSONTestReportBase

JSON格式的测试报告

end_report()

结束测试执行

classmethod get_parser()

获取命令行参数解析器(如果实现)

返回:解析器对象
返回类型:argparse.ArgumentParser
get_testresult_factory()

获取对应的TestResult工厂 :returns ITestResultFactory

classmethod parse_args(args_string)

通过命令行参数构造对象

返回:测试报告
返回类型:cls
class testbase.report.JSONTestReportBase(title='xe8xb0x83xe8xafx95xe6xb5x8bxe8xafx95')

基类:testbase.report.ITestReport

JSON格式的测试报告基类

begin_report()

开始测试执行

end_report()

结束测试执行 :param passed: 测试是否通过 :type passed: boolean

log_filtered_test(loader, testcase, reason)

记录一个被过滤的测试用例 :param loader: 用例加载器 :type loader: TestLoader :param testcase: 测试用例 :type testcase: TestCase :param reason: 过滤原因 :type reason: str

log_load_error(loader, name, error)

记录一个加载失败的用例或用例集 :param loader: 用例加载器 :type loader: TestLoader :param name: 名称 :type name: str :param error: 错误信息 :type error: str

log_loaded_tests(loader, testcases)

记录加载成功的用例

参数:
  • loader (TestLoader) -- 用例加载器
  • testcases (list) -- 测试用例列表
log_record(level, tag, msg, record)

增加一个记录 :param level: 日志级别 :param msg: 日志消息 :param tag: 日志标签 :param record: 日志记录信息 :type level: string :type tag: string :type msg: string :type record: dict

log_test_result(testcase, testresult)

记录一个测试结果 :param testcase: 测试用例 :type testcase: TestCase :param testresult: 测试结果 :type testresult: TestResult

class testbase.report.JSONTestResultFactory

基类:testbase.report.ITestResultFactory

JSON形式TestResult工厂

create(testcase)

创建TestResult对象 :param testcase: 测试用例 :type testcase: TestCase :return TestResult

class testbase.report.StreamTestReport(stream=<open file '<stdout>', mode 'w'>, error_stream=<open file '<stderr>', mode 'w'>, output_testresult=False, output_summary=True)

基类:testbase.report.ITestReport

流形式的测试报告

begin_report()

开始测试执行

end_report()

结束测试执行 :param passed: 测试是否通过 :type passed: boolean

classmethod get_parser()

获取命令行参数解析器(如果实现)

返回:解析器对象
返回类型:argparse.ArgumentParser
get_testresult_factory()

获取对应的TestResult工厂 :returns ITestResultFactory

log_filtered_test(loader, testcase, reason)

记录一个被过滤的测试用例 :param loader: 用例加载器 :type loader: TestLoader :param testcase: 测试用例 :type testcase: TestCase :param reason: 过滤原因 :type reason: str

log_load_error(loader, name, error)

记录一个加载失败的用例或用例集 :param loader: 用例加载器 :type loader: TestLoader :param name: 名称 :type name: str :param error: 错误信息 :type error: str

log_record(level, tag, msg, record={})

增加一个记录 :param level: 日志级别 :param msg: 日志消息 :param tag: 日志标签 :param record: 日志记录信息 :type level: string :type tag: string :type msg: string :type record: dict

log_test_result(testcase, testresult)

记录一个测试结果 :param testcase: 测试用例 :type testcase: TestCase :param testresult: 测试结果 :type testresult: TestResult

classmethod parse_args(args_string)

通过命令行参数构造对象

返回:测试报告
返回类型:cls
class testbase.report.StreamTestResultFactory(stream)

基类:testbase.report.ITestResultFactory

流形式TestResult工厂

create(testcase)

创建TestResult对象 :param testcase: 测试用例 :type testcase: TestCase :return TestResult

dumps()

序列化 :return picklable object

loads(buf)

反序列化 :param buf: dumps返回的序列化后的数据 :type buf: object

class testbase.report.XMLTestReport

基类:testbase.report.ITestReport

XML形式的测试报告

begin_report()

开始测试执行

end_report()

结束测试执行 :param passed: 测试是否通过 :type passed: boolean

classmethod get_parser()

获取命令行参数解析器(如果实现)

返回:解析器对象
返回类型:argparse.ArgumentParser
get_testresult_factory()

获取对应的TestResult工厂 :returns ITestResultFactory

log_filtered_test(loader, testcase, reason)

记录一个被过滤的测试用例 :param loader: 用例加载器 :type loader: TestLoader :param testcase: 测试用例 :type testcase: TestCase :param reason: 过滤原因 :type reason: str

log_load_error(loader, name, error)

记录一个加载失败的用例或用例集 :param loader: 用例加载器 :type loader: TestLoader :param name: 名称 :type name: str :param error: 错误信息 :type error: str

log_record(level, tag, msg, record={})

增加一个记录 :param level: 日志级别 :param msg: 日志消息 :param tag: 日志标签 :param record: 日志记录信息 :type level: string :type tag: string :type msg: string :type record: dict

log_test_result(testcase, testresult)

记录一个测试结果 :param testcase: 测试用例 :type testcase: TestCase :param testresult: 测试结果 :type testresult: XmlResult

classmethod parse_args(args_string)

通过命令行参数构造对象

返回:测试报告
返回类型:cls
class testbase.report.XMLTestResultFactory

基类:testbase.report.ITestResultFactory

XML形式TestResult工厂

create(testcase)

创建TestResult对象 :param testcase: 测试用例 :type testcase: TestCase :return TestResult

testbase.resource Package

资源管理模块

包括:协作机管理及测试资源文件管理

exception testbase.resource.DownloadFileError(url, status_code, msg, headers, data)

基类:exceptions.Exception

download file failed

class testbase.resource.IResourceManagerBackend

基类:object

测试资源管理后端接口定义

acquire_resource(session_id, res_type, res_group, condition)

申请资源

参数:
  • session_id (str) -- 会话ID
  • res_type (str) -- 资源类型
  • res_group -- 资源分组
  • condition (dict) -- 资源属性匹配
返回:

资源

Rtypes:

dict

create_session(testcase=None)

创建会话

参数:testcase (TestCase) -- 使用的测试用例
返回:会话ID
Rtypes:str
destroy_session(session_id)

销毁会话

参数:session_id (str) -- 会话ID
get_file(path)

获取一个文件资源

参数:path (str) -- 相对于resources目录的文件路径
返回:文件绝对路径
Rtypes:str
iter_managed_resource()

查询全部托管的资源(支持初始化&反初始化)

返回:iterator of (res_type, resource)
Rtypes:iterator(res_type, resource)
list_dir(path)

获取一个文件资源

参数:path (str) -- 相对于resources目录的文件夹路径
返回:文件绝对路径
Rtypes:str
release_resource(session_id, res_type, resource_id)

释放资源

参数:
  • session_id (str) -- 会话ID
  • res_type (str) -- 资源类型
  • resource_id (str) -- 资源ID
walk(path)

遍历一个文件路径

参数:path (str) -- 相对于resources目录的文件夹路径
返回:返回一个迭代器,每次迭代对应一个(dir_path, dir_names, file_names)的元组
Rtypes:iterator
class testbase.resource.LocalCSVResourceHandler(csv_path, resource_lock_type=<class 'testbase.resource.LocalResourceLock'>)

基类:testbase.resource.LocalResourceHandler

基于本地CSV文件管理资源

iter_resource(res_type, res_group=None, condition=None)

遍历全部资源(可以按照优先级顺序返回来影响申请资源的优先级)

参数:
  • res_type (str) -- 资源类型
  • res_group (str) -- 资源分组
  • condition (dict) -- 资源属性匹配
返回:

iterator of resource

Rtypes:

iterator(dict)

class testbase.resource.LocalResourceHandler(resource_lock_type=<class 'testbase.resource.LocalResourceLock'>)

基类:object

本地资源处理句柄

acquire_resource(session_id, res_type, res_group, condition)

申请资源

参数:
  • session_id (str) -- 会话ID
  • res_type (str) -- 资源类型
  • res_group (str) -- 资源分组
  • condition (dict) -- 资源属性匹配
返回:

资源

返回类型:

dict

iter_managed_resource(res_type)

查询全部托管的资源(支持初始化&反初始化)

返回:iterator of resource
Rtypes:iterator(dict)
iter_resource(res_type, res_group=None, condition=None)

遍历全部资源(可以按照优先级顺序返回来影响申请资源的优先级)

参数:
  • res_type (str) -- 资源类型
  • res_group (str) -- 资源分组
  • condition (dict) -- 资源属性匹配
返回:

iterator of resource, dict type with key 'id'

Rtypes:

iterator(dict)

release_resource(session_id, res_type, resource_id)

释放资源

参数:
  • session_id (str) -- 会话ID
  • res_type (str) -- 资源类型
  • resource_id (str) -- 资源ID
session_created(session_id, timeout, testcase=None)

通知会话创建

参数:
session_destroyed(session_id)

通知会话销毁

参数:session_id (str) -- 会话ID
class testbase.resource.LocalResourceLock(res_type, resource_id)

基类:object

本地资源锁

release()

释放

try_acquire()

尝试加锁

返回:是否成功
class testbase.resource.LocalResourceManagerBackend

基类:testbase.resource.IResourceManagerBackend

基本本地文件的方式的资源管理

acquire_resource(session_id, res_type, res_group, condition)

申请资源

create_session(testcase=None)

创建会话

destroy_session(sessionid)

销毁会话

get_file(relative_path)

查找某个文件

:type relative_path:string :param relative_path: ,资源文件相对描述符,相对于setting下的资源目录的路径,支持多级目录 :return:返回资源文件的绝对路径

iter_managed_resource()

查询全部托管的资源(支持初始化&反初始化)

返回:iterator(res_type, resource)
list_dir(relative_path)

列出某个目录下的文件

:type relative_path:string :param relative_path: ,资源文件目录相对路径,相对于setting下的资源目录的路径,支持多级目录 :return:返回一个包含资源目录下所有文件或者文件下的绝对路径列表

classmethod register_resource_type(res_type, handler)

注册一个资源类型

release_resource(session_id, res_type, resource_id)

释放资源

walk(path)

获取目录下文件列表

参数:path (str) -- 相对于resources目录的路径,用于遍历文件夹
Return iterators:
 iterator of (dir_path, dirnames, filenames) tuples
exception testbase.resource.ResourceNotAvailable

基类:exceptions.Exception

没有可用的资源

class testbase.resource.Session(backend, session_id)

基类:object

会话

acquire_resource(res_type, res_group=None, condition=None)

申请资源

参数:
  • res_type (str) -- 资源类型
  • res_group -- 资源分组
  • condition (dict) -- 资源属性匹配
返回:

资源

Rtypes:

dict

destroy()

销毁该会话(全部占用的资源会释放)

get_file(path)

获取测试文件资源

参数:path -- 文件引用路径(相对路径)
返回:文件路径
Rtypes:str
list_dir(path)

获取目录下文件列表

参数:path -- 目录引用路径(相对路径)
返回:一个包含该目录下所有文件的绝对路径的list
Rtypes:list[str]
release_resource(res_type, resource_id)

释放资源

参数:
  • res_type (str) -- 资源类型
  • resource_id (str) -- 资源ID
walk(path)

获取目录下文件列表

参数:path (str) -- 相对于resources目录的路径,用于遍历文件夹
Return iterators:
 iterator of (dir_path, dirnames, filenames) tuples
class testbase.resource.TestResourceManager(backend)

基类:object

测试资源管理

create_session(testcase=None)

创建资源使用会话

参数:testcase (TestCase) -- 使用的测试用例
返回:会话
返回类型:Session
iter_managed_resource()

查询全部托管的资源(支持初始化&反初始化)

返回:iterator of (res_type, resource)
Rtypes:iterator(str, dict)
testbase.resource.acquire_resource(res_type, res_group=None, condition=None)

申请资源

参数:
  • res_type (str) -- 资源类型
  • res_group -- 资源分组
  • condition (dict) -- 资源属性匹配
返回:

资源

Rtypes:

dict

testbase.resource.get_file(path)

查找某个文件 :param path: 相对于resources目录的路径,用于查找文件 :type path: str :return:返回资源文件的绝对路径

testbase.resource.iter_resource_paths()

返回测试项目的全部resources目录

返回:
testbase.resource.list_dir(path)

列出某个目录下的文件 :param path: 相对于resources目录的路径,用于查找文件夹 :type path: str :returns :返回一个包含资源目录下所有文件或者文件下的绝对路径的list

testbase.resource.release_resource(res_type, resource_id)

释放资源

参数:
  • res_type (str) -- 资源类型
  • resource_id (str) -- 资源ID
testbase.resource.walk(path)

遍历某个路径

参数:path (str) -- 相对于resources目录的路径,用于遍历文件夹
Return iterators:
 iterator of (dir_path, dirnames, filenames) tuples

testbase.runner Package

TestRunner负责多个测试用例,目前提供三种方式:

  • 单线程执行 TestRunner
  • 多线程执行 ThreadingTestRunner
  • 多进程执行 MultiProcessTestRunner

整个执行逻辑可以用以下伪代码来理解:

for test in tests:
    report.begin_test(test)
    result = report.get_testresult_factory().create(test)
    report.end_test(test, result)
class testbase.runner.BaseTestRunner(report, resmgr_backend=None)

基类:object

测试执行器基类

clean_up()

执行清理动作

classmethod get_parser()

获取命令行参数解析器(如果实现)

返回:解析器对象
返回类型:argparse.ArgumentParser
load(target)

加载测试用例

参数:target (list(TestCase) or list(string) or string or TestCaseSettings) -- 指定要执行的测试用例
返回:测试用例列表
classmethod parse_args(args_string, report, resmgr_backend)

通过命令行参数构造对象

返回:测试报告
返回类型:cls
report

对应的测试报告

返回:ITestReport
resource_setup(plan)

资源初始化

参数:plan (TestPlan) -- 测试计划
resource_teardown(plan)

资源清理

参数:plan (TestPlan) -- 测试计划
run(target)

运行测试

参数:target (list(TestCase) or list(string) or string or TestCaseSettings or TestPlan) -- 指定要执行的测试
run_all_tests(tests)

执行全部的测试用例

参数:tests (list) -- 测试用例对象列表
run_test(test)

执行一个测试用例

参数:test (TestCase) -- 测试用例
返回:boolean - 测试是否通过
class testbase.runner.EnumProcessMsgType

基类:object

多进程间通信用的消息类型

class testbase.runner.MultiProcessTestRunner(report, process_cnt=0, retries=0, resmgr_backend=None)

基类:testbase.runner.BaseTestRunner

使用多进程并发执行用例

多进程并发时,有两个特殊的问题需要处理:

1、测试执行工作进程需要通知TestReport测试用例的执行情况等, 解决方案是: 为每个工作进程提供一个TestReportProxy,TestReportProxy通过消息机制通知 真正的TestReport

2、TestReport需要访问在工作进程的TestResult对象, 解决方案是: 每个工作进程有一个TestResultStubManager,提供给TestReport的是一个TestResultProxy 对象,TestResultProxy通过消息机制和TestResultStubManager通信,来获取真正的TestResult 的信息

clean_up()

执行清理动作

classmethod get_parser()

获取命令行参数解析器(如果实现)

返回:解析器对象
返回类型:argparse.ArgumentParser
classmethod parse_args(args_string, report, resmgr_backend)

通过命令行参数构造对象

返回:测试报告
返回类型:cls
run_all_tests(tests)

执行全部的测试用例

参数:test -- 测试用例对象列表
class testbase.runner.TestCaseSettings(names=None, excluded_names=None, priorities=None, status=None, owners=None, tags=None, excluded_tags=None)

基类:object

目标测试用例配置

filter(testcase)

测试用例过滤函数

参数:testcase (TestCase) -- 测试用例
class testbase.runner.TestReportProxy(worker_id, ctrl_msg_queue, result_factory, result_manager)

基类:testbase.report.ITestReport

测试报告代理

begin_report()

开始测试执行

end_report()

结束测试执行

参数:passed (boolean) -- 测试是否通过
get_testresult_factory()

获取对应的TestResult工厂

返回:ITestResultFactory
log_record(level, tag, msg, record)

增加一个记录

参数:
  • level (string) -- 日志级别
  • tag (string) -- 日志标签
  • msg (string) -- 日志消息
  • record (dict) -- 日志记录信息
log_test_result(testcase, testresult)

记录一个测试结果

参数:
  • testcase (TestCase) -- 测试用例
  • testresult (TestResult) -- 测试结果
class testbase.runner.TestResultFunctionProxy(from_worker, obj_id, func_name)

基类:object

测试结果函数代理

class testbase.runner.TestResultProxy(from_worker, obj_id, passed, testcase)

基类:object

测试结果代理

class testbase.runner.TestResultStubManager(rsp_queue)

基类:object

测试结果桩管理器

add_result(result)

增加一个测试结果

call_result_func(objid, funcname, args, kwargs)

调用一个测试结果的函数

参数:
  • objid (int) -- 对象ID
  • funcname (string) -- 函数名
  • args (tuple) -- 参数
  • kwargs (dict) -- 参数
get_result_attr(objid, attrname)

获取一个测试结果的属性值

参数:
  • objid (int) -- 对象ID
  • attrname (string) -- 属性名
class testbase.runner.TestRunner(report, retries=0, resmgr_backend=None)

基类:testbase.runner.BaseTestRunner

测试执行器

classmethod get_parser()

获取命令行参数解析器(如果实现)

返回:解析器对象
返回类型:argparse.ArgumentParser
classmethod parse_args(args_string, report, resmgr_backend)

通过命令行参数构造对象

返回:测试报告
返回类型:cls
run_all_tests(tests)

执行全部的测试用例

参数:test -- 测试用例对象列表
class testbase.runner.TestWorker(worker_id, ctrl_msg_queue, result_factory, resmgr)

基类:object

多进程执行用例时,执行测试的子进程

current_testcase()

当前正在执行的测试用例

返回:TestCase
recv_message(timeout=None)

接收工作者的答复消息

restart()

重新开始执行

run_testcase(testcase)

分配一个测试用例

参数:testcase (TestCase) -- 要执行的测试用例
send_message(msg)

发送消息到工作者

参数:msg (tuple) -- 消息
start()

开始执行

stop()

结束执行

class testbase.runner.ThreadSafetyReport(report)

基类:testbase.report.ITestReport

TestReport修饰器,保证线程安全

begin_report()

开始测试执行

end_report()

结束测试执行

参数:passed (boolean) -- 测试是否通过
get_testresult_factory()

获取对应的TestResult工厂

返回:ITestResultFactory
log_filtered_test(loader, testcase, reason)

记录一个被过滤的测试用例 :param loader: 用例加载器 :type loader: TestLoader :param testcase: 测试用例 :type testcase: TestCase :param reason: 过滤原因 :type reason: str

log_load_error(loader, name, error)

记录一个加载失败的用例或用例集 :param loader: 用例加载器 :type loader: TestLoader :param name: 名称 :type name: str :param error: 错误信息 :type error: str

log_loaded_tests(loader, testcases)

记录加载成功的用例

参数:
  • loader (TestLoader) -- 用例加载器
  • testcases (list) -- 测试用例列表
log_record(level, tag, msg, record)

增加一个记录

参数:
  • level (string) -- 日志级别
  • msg (string) -- 日志消息
  • tag (string) -- 日志标签
  • record (dict) -- 日志记录信息
log_test_result(testcase, testresult)

记录一个测试结果

参数:
  • testcase (TestCase) -- 测试用例
  • testresult (TestResult) -- 测试结果
class testbase.runner.ThreadingTestRunner(report, thread_cnt=0, retries=0, resmgr_backend=None)

基类:testbase.runner.BaseTestRunner

使用多线程并发执行用例

classmethod get_parser()

获取命令行参数解析器(如果实现)

返回:解析器对象
返回类型:argparse.ArgumentParser
classmethod parse_args(args_string, report, resmgr_backend)

通过命令行参数构造对象

返回:测试报告
返回类型:cls
run_all_tests(tests)

执行全部的测试用例

参数:test -- 测试用例对象列表

testbase.serialization Package

测试用例序列化和反序列化

testbase.serialization.dumps(testcase)

序列化测试用例

参数:testcase (TestCase) -- 测试用例
testbase.serialization.loads(buf)

反序列化测试用例

参数:buf (dict) -- 测试用例序列化数据
返回:TestCase

testbase.testcase Package

测试用例基类模块

class testbase.testcase.Environ

基类:dict

测试环境类

用法说明: 它是一个继承了字典类型的单实例类。使用时,需先实例化该类,如: from testbase.testcase import Environ env = Environ() env保存了用例运行时的一些测试环境变量。

测试环境变量分为3个部分:

1、在测试执行时,env则存储了由测试计划定义的用例环境变量,使用方法如下:

from testbase.testcase import Environ
env = Environ()
print(env_
# 输出
env = {
    'ASSERTTEST':'True',  #注意:这里key是全字母大写
}

2、测试用例基类testbase.testcase.TestCase的构造函数中也实例化了Environ类(变量名为environ), 并且,保存了当时执行用例类的类名和类说明,使用方法如下:

#todo: 执行用例中使用evniron,用于打印用例名称和用例说明
from testbase.testcase import TestCase
class YourTest(TestCase):
    def runTest(self):
        print(self.environ['TestName'])
        print(self.environ['TestDoc'])

3、Environ还可以用于设置自定义的环境变量,使用方法如下:

from testbase.testcase import Environ
env = Environ()
env['YourEnvKey'] = "EnvValue" 
print(env['YourEnvKey'])
class testbase.testcase.ITestCaseRunner

基类:object

测试用例执行器接口定义

run(testcase, testresult_factory)

执行一个测试用例

参数:

:return TestResult/TestResultCollection - 测试结果

class testbase.testcase.RepeatTestCaseRunner(case_runner_class=None)

基类:testbase.testcase.ITestCaseRunner

重复执行的用例执行器

可以通过设置测试用例的类属性为此runner实例来实现指定测试用例 执行多次。测试用例执行时可以访问成员变量iteration来判断当前是 第几次执行。

使用示例如下:

class HelloRepeatTest(TestCase):
    '示例用例'
    case_runner = RepeatTestCaseRunner()
    owner = "foo"
    timeout = 1
    status = TestCase.EnumStatus.Ready
    priority = TestCase.EnumPriority.Normal
    
    def run_test(self):
        self.log_info("第%s次执行测试"%self.iteration)
run(testcase, testresult_factory)

执行一个测试用例

参数:

:rtype TestResult/TestResultCollection - 测试结果

class testbase.testcase.SeqTestCaseRunner

基类:testbase.testcase.ITestCaseRunner

顺序执行的用例的执行器

run(testsuite, testresult_factory)

执行一个顺序执行的测试用例套

参数:

:return TestResult/TestResultCollection - 测试结果

class testbase.testcase.SeqTestSuite(testcases)

基类:testbase.testcase.TestSuite

顺序执行的测试用例套

dumps()

序列化

loads(buf)

反序列化

test_class_name

返回测试用例名字(不同测试用例的名字不同)

返回类型:str
test_doc

测试用例说明

返回类型:str
test_name

返回测试用例实例的名字

返回类型:str
test_resmgr

资源管理器

test_result

将最后一个执行的用例结果,作为Suite的结果

class testbase.testcase.TestCase(testdata=None, testdataname=None, attrs=None)

基类:object

测试用例基类

所有测试用例都最终从此基类继承。测试用例的测试脚本主要实现在"runTest()"中,

而当用例需要初始化和清理测试环境时则分别重写"preTest()"和"postTest()"函数。

class EnumPriority

基类:object

测试用例优先级枚举类

class EnumStatus

基类:object

测试用例状态枚举类

Attention:如果因为特殊原因需要暂时屏蔽某个用例的任务执行(比如有功能缺陷从而导致执行失败), 则可以先置为该字段为Suspend,等到可用的时候再将该字段置为Ready
TestClassName

返回测试用例名字(不同测试用例的名字不同)

返回类型:str
TestDoc

测试用例说明

返回类型:str
TestName

返回测试用例实例的名字

返回类型:str
assertEqual(message, actual, expect=True)

检查实际值和期望值是否相等,不能则测试用例失败

参数:
  • message -- 检查信息
  • actual -- 实际值
  • expect -- 期望值(默认:True)
返回:

True or False

assertMatch(message, actual, expect)

检查actual和expect是否模式匹配,不匹配则记录一个检查失败

参数:
  • message (string) -- 失败时记录的消息
  • actual (string) -- 需要匹配的字符串
  • expect (string) -- 要匹配的正则表达式
返回:

匹配成果

assert_(message, value)

测试断言,如果value的值不为真,则用例失败,输出对应信息

:param message:断言失败时的提示消息 :type message: str :param value:用于判断的值 :type value: bool或

assert_equal(message, actual, expect=True)

检查实际值和期望值是否相等,不能则测试用例失败

参数:
  • message -- 检查信息
  • actual -- 实际值
  • expect -- 期望值(默认:True)
返回:

True or False

assert_match(message, actual, expect)

检查actual和expect是否模式匹配,不匹配则记录一个检查失败

参数:
  • message (string) -- 失败时记录的消息
  • actual (string) -- 需要匹配的字符串
  • expect (string) -- 要匹配的正则表达式
返回:

匹配成果

casedata

测试数据

返回类型:list
casedataname

测试数据标识

返回类型:str
cleanTest()

测试用例反初始化。慎用此函数,尽量将清理放到postTest里。

clean_test()

测试用例反初始化。慎用此函数,尽量将清理放到postTest里。

debug_run()

本地调试测试用例

debug_run_one(name=None)

本地调试测试用例,给数据驱动的用例使用,只执行一个用例

参数:name -- 测试数据名称,如果不指定,执行第一个数据的用例
environ

环境变量

返回类型:Environ
fail(message)

测试用例失败

参数:message (string) -- 要Log的信息
get_extra_fail_record()

当错误发生时,获取需要额外添加的日志记录和附件信息

返回类型:dict,dict - 日志记录,附件信息
initTest(testresult)

初始化测试用例。慎用此函数,尽量将初始化放到preTest里。

参数:testresult (TestResult) -- 测试结果
init_test(testresult)

初始化测试用例。慎用此函数,尽量将初始化放到preTest里。

参数:testresult (TestResult) -- 测试结果
logInfo(info)

Log一条信息

参数:info (string) -- 要Log的信息
log_info(info)

Log一条信息

参数:info (string) -- 要Log的信息
postTest()

测试环境清理

post_test()

测试环境清理

preTest()

测试环境初始化

pre_test()

测试环境初始化

run()

本地调试测试用例

runTest()

运行测试用例

run_test()

运行测试用例

startStep(stepinfo)

开始执行一个测试步骤

参数:stepinfo (str) -- 步骤描述
start_step(stepinfo)

开始执行一个测试步骤

参数:stepinfo (str) -- 步骤描述
test_class_name

返回测试用例名字(不同测试用例的名字不同)

返回类型:str
test_dir

测试用例执行的临时目录

返回类型:str
test_doc

测试用例说明

返回类型:str
test_extra_info

测试用例额外信息

test_name

返回测试用例实例的名字

返回类型:str
test_resmgr

资源管理器

test_resources

资源管理使用接口

test_result

对应的测试结果

返回类型:TestResult
waitForEqual(message, obj, prop_name, expected, timeout=10, interval=0.5)

每隔interval检查obj.prop_name是否和expected相等,如果在timeout时间内都不相等,则测试用例失败

参数:
  • message -- 失败时的输出信息
  • obj -- 需要检查的对象
  • prop_name (string) -- 需要检查的对象的属性名,支持多层属性
  • expected -- 期望的obj.prop_name值
  • timeout -- 超时秒数
  • interval -- 重试间隔秒数
返回:

True or False

waitForMatch(message, obj, prop_name, expected, timeout=10, interval=0.5)

每隔interval检查obj.prop_name是否和正则表达式expected是否匹配,如果在timeout时间内都不相等,则测试用例失败

参数:
  • message -- 失败时的输出信息
  • obj -- 需要检查的对象
  • prop_name (string) -- 需要检查的对象的属性名, obj.prop_name返回字符串
  • expected -- 需要匹配的正则表达式
  • timeout -- 超时秒数
  • interval -- 重试间隔秒数
返回:

True or False

wait_for_equal(message, obj, prop_name, expected, timeout=10, interval=0.5)

每隔interval检查obj.prop_name是否和expected相等,如果在timeout时间内都不相等,则测试用例失败

参数:
  • message -- 失败时的输出信息
  • obj -- 需要检查的对象
  • prop_name (string) -- 需要检查的对象的属性名,支持多层属性
  • expected -- 期望的obj.prop_name值
  • timeout -- 超时秒数
  • interval -- 重试间隔秒数
返回:

True or False

wait_for_match(message, obj, prop_name, expected, timeout=10, interval=0.5)

每隔interval检查obj.prop_name是否和正则表达式expected是否匹配,如果在timeout时间内都不相等,则测试用例失败

参数:
  • message -- 失败时的输出信息
  • obj -- 需要检查的对象
  • prop_name (string) -- 需要检查的对象的属性名, obj.prop_name返回字符串
  • expected -- 需要匹配的正则表达式
  • timeout -- 超时秒数
  • interval -- 重试间隔秒数
返回:

True or False

class testbase.testcase.TestCasePriority

基类:object

测试用例优先级

Attention:此类将会被移除,请使用TestCase.EnumPriority
class testbase.testcase.TestCaseRunner

基类:testbase.testcase.ITestCaseRunner

负责执行一个测试用例

如果一个测试用例没有指定case_runner类变量,则默认都使用TestCaseRunner来执行这个用例。 测试用例可以自定义和TestCaseRunner接口兼容的runner类,并设置case_runner类变量来实现 自定义一个测试用例的执行逻辑,以下是TestCaseRunner的接口定义

run(testcase, testresult_factory)

执行一个测试用例

参数:
返回类型:

TestResult/TestResultCollection - 测试结果

setup(testcase, testresult)

测试执行初始化

参数:
  • testcase (TestCase) -- 执行的测试用例
  • testresult (TestResult) -- 测试用例结果
teardown(testcase, testresult)

测试执行清理

参数:
  • testcase (TestCase) -- 执行的测试用例
  • testresult (TestResult) -- 测试用例结果
class testbase.testcase.TestCaseStatus

基类:object

测试用例状态

Attention:此类将会被移除,请使用TestCase.EnumStatus
class testbase.testcase.TestCaseType

基类:type

测试用例元类型

class testbase.testcase.TestSuite

基类:object

测试用例套

dumps()

序列化

loads(buf)

反序列化

suite_class_name

测试套类名称

testbase.testcase.debug_run_all()

调试执行当前脚本的全部用例

testbase.testresult Package

测试结果模块

参考logging模块的设计,使用示例如下:

result = TestResult()
result.add_handler(StreamResultHandler())
result.begin_test()
result.start_step('a step')
result.info('test')
result.end_test()

其中result.info接口可以传record扩展信息,比如:

result.error('', '异常发生', traceback=traceback.format_exc())

同logger一样,TestResult对象保证对所有的ITestResultHandler的调用都是线程安全的,可以通过实现 ITestResultHandler来实现一个新的Handler,详细请参考ITestResultHandler接口

class testbase.testresult.EmptyResult

基类:testbase.testresult.TestResultBase

不输出

class testbase.testresult.EnumLogLevel

基类:object

日志级别

class testbase.testresult.HtmlResult(testcase)

基类:testbase.testresult.JSONResult

html test result

class testbase.testresult.JSONResult(testcase)

基类:testbase.testresult.TestResultBase

JSON格式的结果

handle_log_record(level, msg, record, attachments)

处理一个日志记录

参数:
  • level (string) -- 日志级别,参考EnumLogLevel
  • msg (string) -- 日志消息
  • record (dict) -- 日志记录
  • attachments (dict) -- 附件
handle_step_begin(msg)

处理一个测试步骤的开始

参数:msg (string) -- 测试步骤名称
handle_step_end(passed)

处理一个测试步骤的结束

参数:passed (boolean) -- 测试步骤是否通过
handle_test_begin(testcase)

处理一个测试用例执行的开始

参数:testcase (TestCase) -- 测试用例
handle_test_end(passed)

处理一个测试用例执行的结束

参数:passed (boolean) -- 测试用例是否通过
class testbase.testresult.StreamResult(stream=<open file '<stdout>', mode 'w'>)

基类:testbase.testresult.TestResultBase

测试用例stream输出

handle_log_record(level, msg, record, attachments)

处理一个日志记录

参数:
  • level (string) -- 日志级别,参考EnumLogLevel
  • msg (string) -- 日志消息
  • record (dict) -- 日志记录
  • attachments (dict) -- 附件
handle_step_begin(msg)

处理一个测试步骤的开始

参数:msg (string) -- 测试步骤名称
handle_step_end(passed)

处理一个测试步骤的结束

参数:passed (boolean) -- 测试步骤是否通过
handle_test_begin(testcase)

处理一个测试用例执行的开始

参数:testcase (TestCase) -- 测试用例
handle_test_end(passed)

处理一个测试用例执行的结束

参数:passed (boolean) -- 测试用例是否通过
class testbase.testresult.TestResultBase

基类:object

测试结果基类

此类的职责如下: 1、提供测试结果基本接口 2、保证线程安全 2、测试是否通过之逻辑判断

begin_step(msg)

开始一个测试步骤

参数:msg (string) -- 测试步骤名称
begin_test(testcase)

开始执行测试用例

参数:testcase (TestCase) -- 测试用例
begin_time

测试用例开始时间

返回:float
debug(msg, record=None, attachments=None)

处理一个DEBUG日志

end_test()

结束执行测试用例

end_time

测试用例结束时间

返回:float
error(msg, record=None, attachments=None)

处理一个ERROR日志

exception(msg, record=None, attachments=None)

处理一个DEBUG日志

failed_reason

用例测试不通过的错误原因

返回:str
handle_log_record(level, msg, record, attachments)

处理一个日志记录

参数:
  • level (string) -- 日志级别,参考EnumLogLevel
  • msg (string) -- 日志消息
  • record (dict) -- 日志记录
  • attachments (dict) -- 附件
handle_step_begin(msg)

处理一个测试步骤的开始

参数:msg (string) -- 测试步骤名称
handle_step_end(passed)

处理一个测试步骤的结束

参数:passed (boolean) -- 测试步骤是否通过
handle_test_begin(testcase)

处理一个测试用例执行的开始

参数:testcase (TestCase) -- 测试用例
handle_test_end(passed)

处理一个测试用例执行的结束

参数:passed (boolean) -- 测试用例是否通过
info(msg, record=None, attachments=None)

处理一个INFO日志

log_record(level, msg, record=None, attachments=None)

处理一个日志记录

参数:
  • level (string) -- 日志级别,参考EnumLogLevel
  • msg (string) -- 日志消息
  • record (dict) -- 日志记录
  • attachments (dict) -- 附件
passed

测试是否通过

返回:True or False
testcase

对应的测试用例 :returns: TestCase

warning(msg, record=None, attachments=None)

处理一个WARNING日志

class testbase.testresult.TestResultCollection(results, passed)

基类:list

测试结果集合

passed

测试是否通过

返回:boolean
class testbase.testresult.XmlResult(testcase)

基类:testbase.testresult.TestResultBase

xml格式的测试用例结果

file_path

xml文件路径

返回:str
handle_log_record(level, msg, record, attachments)

处理一个日志记录

参数:
  • level (string) -- 日志级别,参考EnumLogLevel
  • msg (string) -- 日志消息
  • record (dict) -- 日志记录
  • attachments (dict) -- 附件
handle_step_begin(msg)

处理一个测试步骤的开始

参数:msg (string) -- 测试步骤名称
handle_step_end(passed)

处理一个测试步骤的结束

参数:passed (boolean) -- 测试步骤是否通过
handle_test_begin(testcase)

处理一个测试用例执行的开始

参数:testcase (TestCase) -- 测试用例
handle_test_end(passed)

处理一个测试用例执行的结束

参数:passed (boolean) -- 测试用例是否通过
toxml()

返回xml文本

:returns string - xml文本

testbase.testresult.smart_text_by_lines(s)

将任意字符串转换为UTF-8编码

testbase.dist Package

打包

class testbase.dist.DistGenerator(version)

基类:object

Build dist packages

class testbase.dist.VirtuelEnv(dist_pkg_path, path=None, recreate=False)

基类:object

virtual env for QTA test project

activate()

activate virtuelenv on current processs

Logic:
  1. Call from 'qta-manage', then create virtualenv
  2. Activate it
  3. Set OS environ 'QTAF_VENV'
  4. Create child process, replace 'qta-manage' with 'qta-manage-venv'
    1. Call from 'qta-manage-venv'
    2. OS environ 'QTAF_VENV' detected, ignore virtualenv creation code

testbase.plan Package

Test plan

class testbase.plan.TestPlan

基类:object

测试计划

debug_run(report=None, resmgr_backend=None)

调试执行

get_test_target()

获取被测对象

返回:被测对象信息
Rtypes:dict
get_tests()

获取测试用例定义

Rtypes:str/list(str)/list(TestCase)/TestCaseSettings
resource_setup(report, restype, resource)

测试资源初始化

resource_teardown(report, restype, resource)

测试资源清理

test_setup(report)

测试初始化步骤

test_teardown(report)

测试清理步骤

testbase.util Package

共用类模块

testbase.util.ForbidOverloadMethods(func_name_list)

生成metaclass用于指定基类禁止子类重载函数

class testbase.util.LazyInit(obj, propname, init_func)

基类:object

实现延迟初始化

使用方式示例:

class _Win32Window(object)
    def click(self):
        #......
class Control(object):
    def __init__(self, locator):
        self._locator = locator
        self._initobj = LazyInit(self, '_initobj', self._init_window)
    def _init_window(self):
        return _Win32Window(self._locator)
    def click(self):
        return self._initobj.click()
        
ctrl = Control("/Name=xxx)
ctrl.click()  # <-- call _init_window
ctrl.click()
class testbase.util.Singleton(name, bases, dic)

基类:type

单实例元类,用于某个类需要实现单例模式。 使用方式示例如下:

import six
class MyClass(with_metaclass(Singleton, object)):
    def __init__(self, *args, **kwargs):
        pass
class testbase.util.ThreadGroupLocal

基类:object

使用线程组本地存储的元类

  • 当配合ThreadGroupScope使用,类似threading.local()提供的TLS变种,一个线程和其子孙线程共享一个存储

详细使用方式请参考ThreadGroupScope类

  • 当不在ThreadGroupScope中使用时,行为和threading.local()一致
class testbase.util.ThreadGroupScope(name)

基类:object

指定线程组作用域,进入这个作用域的线程,以及在其作用域内创建的线程都同属于一个线程组

使用示例如下:

def _thread_proc():
    ThreadGroupLocal().counter +=1
    
with ThreadGroupScope("test_group"):
    ThreadGroupLocal().counter = 0
    t = threading.Thread(target=_thread_proc)
    t.start()
    t.join()
    t = threading.Thread(target=_thread_proc)
    t.start()
    t.join()
    assert ThreadGroupLocal().counter == 2
static current_scope()

返回当前线程所在的线程组作用域,如果不存在于任务线程组作用域,则返回None

class testbase.util.Timeout(timeout=10, interval=0.5)

基类:object

TimeOut类,实现超时重试逻辑

check(func, expect)

多次检查func的返回值是否符合expect设定的期望值,如果设定时间内满足,则返回True,否则返回False

参数:
  • func -- 尝试调用的函数
  • expect -- 设定的期望值

:returns bool - 检查是否符合预期

retry(func, args, exceptions=(), resultmatcher=None, nothrow=False)

多次尝试调用函数,成功则并返回调用结果,超时则根据选项决定抛出TimeOutError异常。

参数:
  • func -- 尝试调用的函数
  • args (dict或tuple) -- func函数的参数
  • exceptions (tuple类型,tuple元素是异常类定义,如QPathError, 而不是异常实例,如QPathError()) -- 调用func时抛出这些异常,则重试。 如果是空列表(),则不捕获异常。
  • resultmatcher (函数指针类型) --
    函数指针,用于验证第1个参数func的返回值。
    默认值为None,表示不验证func的返回值,直接返回。

    其函数原型为:

    def result_match(ret): # 参数ret为func的返回值
    pass
    当result_match返回True时,直接返回,否则继续retry。

:type nothrow:bool :param nothrow:如果为True,则不抛出TimeOutError异常 :return: 返回成功调用func的结果

waitObjectProperty(obj, property_name, waited_value, regularMatch=False)
通过比较obj.property_name和waited_value,等待属性值出现。
如果属性值obj.property_name是字符类型则waited_value做为正则表达式进行比较。 比较成功则返回,超时则抛出TimeoutError异常。
参数:
  • obj -- 对象
  • property_name -- 要等待的obj对象的属性名
  • waited_value -- 要比较的的属性值,支持多层属性
  • regularMatch -- 参数 property_name和waited_value是否采用正则表达式的比较。 默认为不采用(False)正则,而是采用恒等比较
class testbase.util.classproperty(getter)

基类:object

类属性修饰器

testbase.util.get_thread_traceback(thread)

获取用例线程的当前的堆栈

参数:thread (Thread) -- 要获取堆栈的线程
testbase.util.smart_binary(s, encoding='utf8', decoding=None)

convert any text or binary to binary of specified encoding

testbase.util.smart_bytify(obj, encoding='utf-8', decoding=None)

recursively convert objects from string types to binary

testbase.util.smart_strfy(obj, decoding=None)

recursively convert objects from binary to text

testbase.util.smart_text(s, decoding=None)

convert any text or binary to text py2 text: utf-8 bytes py3 text: unicode

testbase.util.to_pretty_xml(doc, encoding='utf-8')

we need to ensure each line to be binary type

配置项说明文档

DEBUG

当值为True时,表示为调试模式下执行,否则为正式环境下执行。

适用3.30.26或以上版本。

DATA_DRIVE

默认为False,当为True时表示使用全局数据驱动,此时需要通过DATA_SOURCE指定数据文件。

适用5.0.0或以上版本。

DATA_SROUCE

如果是字符串,则用于指定全局数据驱动的数据文件路径,使用相对路径,“/”标识测试项目的根目录;指定的数据文件使用Python代码格式,其中需要定义一个列表或字典兼容类型的“DATASET”变量,表示测试数据驱动的源数据。

如果是列表或字典兼容类型,则表示数据驱动的数据源。

适用5.0.0或以上版本。

PROJECT_MODE

指定项目的运行管理模式:独立模式(standalone)和标准模式(standard),默认为独立模式。

适用5.0.97或以上版本。

PROJECT_ROOT

仅标准模式下配置文件中的改设置才有效,用于指定项目的根目录,此目录会加到python path中。在独立模式下PROJECT_ROOT的指会自动推导得到,因此不能修改和设置。

适用5.0.97或以上版本。

INSTALLED_APPS

仅标准模式下配置文件中的改设置才有效,用于指定已安装的QTAF库。在独立模式下INSTALLED_APPS的值通过读取exlib/installed_libs.txt得到,,因此不能修改和设置。

适用5.0.97或以上版本。

环境变量配置

下面环境变量可以用来告诉QTAF,配置文件存放的位置,环境变量指定的优先级是最高的

QTAF_SETTINGS_MODULE

指定用户自定义的配置模块,python在运行时可以找到的模块,支持多级路径,例如:myproject.settings_20160705