QTA Testbase文档

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

使用文档

使用前准备

Python版本依赖

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

注解

如果使用的是64版本的Windows,也请安装32版本的Python,目前的QTA的部分Driver仅支持32位版本。

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

通过pip工具安装

安装方法如下:

pip install qtaf

通过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:通过
测试用例最终结果: 通过
============================================================

测试断言

上面的测试代码没有任何测试的逻辑,并不算是一个完整的测试用例。下面我们通过一个例子来介绍QTA的两个测试检查接口。

假设我们需要测试一个字符串拼接的函数:

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_equal("检查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_combine2:

def string_combine2(a,b):
   return a+'b'

以为以上的实现是有问题,执行结果必然是不通过的:

============================================================
测试用例:StrCombineTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
----------------------------------------
步骤1: 测试字符串拼接
ASSERT: 检查string_combine2调用结果
   实际值:xxXb
   期望值:xxXyy
  File "D:\workspace\qtaf5\test\hellotest.py", line 87, in run_test
============================================================
测试用例开始时间: 2016-02-02 14:11:45
测试用例结束时间: 2016-02-02 14:11:45
测试用例执行时间: 00:00:0.00
测试用例步骤结果:  1:失败
测试用例最终结果: 失败
============================================================

可以看到除了测试不通过外,测试结果还显示了断言失败的详细信息,包括预期值、实际值和对应的代码行。

这个就是QTA提供的测试断言的函数接口,其详细的定义如下:

def assert_equal(self, message, actual, expect=True):
    '''检查实际值和期望值是否相等,不同则测试用例失败

   :param message: 检查信息
   :param actual: 实际值
   :param expect: 期望值(默认:True)
   :return: True or False
    '''

除了这个,QTA还提供另一个版本的断言函数:

def assert_match(self, message, actual, expect):
    '''检查actual和expect是否模式匹配,不匹配则记录一个检查失败

    :type message: string
    :param message: 失败时记录的消息
    :type actual: string
    :param actual: 需要匹配的字符串
    :type expect: string
    :param expect: 要匹配的正则表达式
    :return: True or False
    '''

assert_match和assert_equal的区别是,assert_match使用的是正则匹配而不是严格匹配,比如:

self.assert_equal("严格匹配断言", "XXX", "X*")

以上的断言是不通过的,但是对于下面的正则断言是通过的:

self.assert_match("正则匹配断言", "XXX", "X*")

assert_match和assert_equal相比,还有一个区别就是,assert_match只支持字符串或字符串兼容的类型的值的检查;但是assert_equal可以支持大部分类型的值的检查。

忙等待检查

以上的两个断言的检查的接口,都是检查某个时刻的被测系统的状态。但是对于一些系统,特别是UI,如果仅仅调用assert_equal和assert_match接口去检查当前的状态其实是不恰当的。

例如,一个表单的UI界面,如果点击“提交”后,我们需要检查“提交”按钮变为不可点击的状态,测试用例可能是这样的:

form.controls['提交按钮'].click()
self.assert_equal("检查“提交”按钮变为不可点击的状态", form.controls['提交按钮'].enable, False)

上面的测试用例,存在一个问题,就是点击“提交”之后,“提交”按钮的状态的更新并不是同步的,可能由于被测系统响应慢了一点点,就会导致测试检查不通过,所以上面的测试用例代码段应该修改为:

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秒还是可以点击")

以上的测试代码是在2秒之内,多次去检查“提交”按钮的状态是否符合预期。通过这样的“平滑”的方式,就可以避免由于被测系统状态同步问题而导致测试不稳定。

但是上面的测试代码还是相当复杂的,因此QTA测试用例提供了两个接口来帮忙解决这类问题:

def wait_for_equal(self, message, obj, prop_name, expected, timeout=10, interval=0.5):
    '''每隔interval检查obj.prop_name是否和expected相等,如果在timeout时间内都不相等,则测试用例失败

    :param message: 失败时的输出信息
    :param obj: 需要检查的对象
    :type prop_name: string
    :param prop_name: 需要检查的对象的属性名,支持多层属性
    :param expected: 期望的obj.prop_name值
    :param timeout: 超时秒数
    :param interval: 重试间隔秒数
    :return: True or False
    '''

def wait_for_match(self, message, obj, prop_name, expected, timeout=10, interval=0.5):
    '''每隔interval检查obj.prop_name是否和正则表达式expected是否匹配,如果在timeout时间内都不相等,则测试用例失败

    :param message: 失败时的输出信息
    :param obj: 需要检查的对象
    :type prop_name: string
    :param prop_name: 需要检查的对象的属性名, obj.prop_name返回字符串
    :param expected: 需要匹配的正则表达式
    :param timeout: 超时秒数
    :param interval: 重试间隔秒数
    :return: True or False
    '''

这两个其实就是assert_equal和assert_match的忙等待检查版本,通过wait_for系列的接口,上面的测试代码就可以简化为:

form.controls['提交按钮'].click()
self.wait_for_equal("检查提交按钮变为不可点击", form.controls['提交按钮'], "enable", False,
                    timeout=2, interval=0.2)

测试执行控制

QTA测试用例的代码的执行控制逻辑和一般Python的代码是类似的,所以除了执行过程中出现Python异常或用例执行超时,测试用例会一直执行。而且,即使是assert_和wait_for_系列的接口失败了,也会继续执行,比如下面的例子:

class CtrlTest(TestCase):
    '''流程控制测试
    '''
    owner = "foo"
    status = TestCase.EnumStatus.Ready
    priority = TestCase.EnumPriority.Normal
    timeout = 1

    def run_test(self):
        #---------------------------
        self.start_step("断言失败")
        #---------------------------
        self.assert_equal("检查断言", True, False)

        #---------------------------
        self.start_step("第二个步骤")
        #---------------------------
        self.log_info("hello")

上面的第一个测试步骤中,有一个断言是必然失败的,但是第二个测试步骤还是会被正常执行:

============================================================
测试用例:CtrlTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
----------------------------------------
步骤1: 断言失败
ASSERT: 检查断言
   实际值:True
   期望值:False
  File "D:\workspace\qtaf5\test\hellotest.py", line 86, in run_test
----------------------------------------
步骤2: 第二个步骤
INFO: hello
============================================================
测试用例开始时间: 2016-02-02 15:27:29
测试用例结束时间: 2016-02-02 15:27:29
测试用例执行时间: 00:00:0.00
测试用例步骤结果:  1:失败 2:通过
测试用例最终结果: 失败
============================================================

注解

对于断言失败的执行逻辑处理,这个是QTA测试框架和其他一般测试框架比较大的差异点,设计测试用例是需要注意。

测试环境初始化和清理

在前面的例子中,我们在测试用例类的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语言的含义表示的是调用基类的方法,虽然不是必定需要的,但是大部分情况下还是推荐这样做;因为这样做可以保证基类的初始化和清理的接口会被执行。

执行测试用例

对于测试用例的执行,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 runTest(self):
        self.logInfo('第%s次测试执行'%self.iteration)


if __name__ == '__main__':
    HelloTest().debug_run()

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

  • 增加repeat属性,用于指定要重复执行的次数
  • case_runner属性,指定了一个“testbase.testcase.RepeatTestCaseRunner”实例

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

============================================================
测试用例:RepeatTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
INFO: 第0次测试执行
============================================================
测试用例开始时间: 2015-07-16 20:17:11
测试用例结束时间: 2015-07-16 20:17:11
测试用例执行时间: 00:00:0.00
测试用例步骤结果:  1:通过
测试用例最终结果: 通过
============================================================
============================================================
测试用例:RepeatTest 所有者:foo 优先级:Normal 超时:1分钟
============================================================
INFO: 第1次测试执行
============================================================
测试用例开始时间: 2015-07-16 20:17:11
测试用例结束时间: 2015-07-16 20:17:11
测试用例执行时间: 00:00:0.00
测试用例步骤结果:  1:通过
测试用例最终结果: 通过
============================================================

可以看到测试用例被执行了两次,而且每次执行的时候,用例成员变量iteration都会增加1。

控制执行顺序

对于一些极端的情况下,需要控制测试用例的执行顺序。比如执行测试用例A、B、C需要按照一定先A、后B、再C的顺序来执行。

警告

QTA不推荐测试用例之间存在依赖关系,这样对于用例的可读性和后续的维护都会带来麻烦,所以不推荐控制用例按照顺序来执行。

例如下面一个控制执行顺序的例子:

from testbase import TestCase
from testbase.testcase import RepeatTestCaseRunner

class TestA(TestCase):
    '''测试示例
    '''
    timeout = 1
    owner = "foo"
    status = TestCase.EnumStatus.Ready
    priority = TestCase.EnumPriority.Normal

    def run_test(self):
        pass

class TestB(TestCase):
    '''测试示例
    '''
    timeout = 1
    owner = "foo"
    status = TestCase.EnumStatus.Ready
    priority = TestCase.EnumPriority.Normal
    case_runner = RepeatTestCaseRunner()
    repeat = 2

    def run_test(self):
        pass

class TestC(TestCase):
    '''测试示例
    '''
    timeout = 1
    owner = "foo"
    status = TestCase.EnumStatus.Ready
    priority = TestCase.EnumPriority.Normal

    def run_test(self):
        pass

__qtaf_seq_tests__ = [TestA, TestB, TestC]

if __name__ == '__main__':
    from testbase.testcase import debug_run_all
    debug_run_all()

以上用例和普通的用例完全一致,不一样的地方是在模块中定义了变量qtaf_seq_tests ,这个变量就是用来指定测试用例的执行顺序。需要注意的是,如果要指定测试用例按照顺序执行,这些用例的实现都必须放在同一个代码文件中,这样限制的目的是为了提高代码的可读性。

以上的例子的执行结果如下:

============================================================
测试用例:TestA 所有者:foo 优先级:Normal 超时:1分钟
============================================================
============================================================
测试用例开始时间: 2015-07-16 20:24:46
测试用例结束时间: 2015-07-16 20:24:46
测试用例执行时间: 00:00:0.00
测试用例步骤结果:  1:通过
测试用例最终结果: 通过
============================================================
============================================================
测试用例:TestB 所有者:foo 优先级:Normal 超时:1分钟
============================================================
============================================================
测试用例开始时间: 2015-07-16 20:24:46
测试用例结束时间: 2015-07-16 20:24:46
测试用例执行时间: 00:00:0.00
测试用例步骤结果:  1:通过
测试用例最终结果: 通过
============================================================
============================================================
测试用例:TestB 所有者:foo 优先级:Normal 超时:1分钟
============================================================
============================================================
测试用例开始时间: 2015-07-16 20:24:46
测试用例结束时间: 2015-07-16 20:24:46
测试用例执行时间: 00:00:0.00
测试用例步骤结果:  1:通过
测试用例最终结果: 通过
============================================================
============================================================
测试用例:TestC 所有者:foo 优先级:Normal 超时:1分钟
============================================================
============================================================
测试用例开始时间: 2015-07-16 20:24:46
测试用例结束时间: 2015-07-16 20:24:46
测试用例执行时间: 00:00:0.00
测试用例步骤结果:  1:通过
测试用例最终结果: 通过
============================================================

自定义执行方式

对于一般的测试用例的执行,QTA是按照下面的流程处理的:

  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 = "eeelin"
   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。

管理数据驱动测试用例

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

调试数据驱动测试用例

数据驱动测试用例本地调试的时候,可以和一般的测试用例一样:

if __name__ == '__main__':
    DataHelloTest().debug_run()

但是上面的方式会执行所有的数据驱动的用例,如果需要指定测试用例使用具体某一个测试数据,对于list类型的数据可以:

if __name__ == '__main__':
    DataHelloTest()[0].debug_run()

上面的例子指调试执行第一个数据驱动生成的用例。

如果是dict类型的测试数据,可以:

if __name__ == '__main__':
    DataHelloTest()["key"].debug_run()

上面的例子会调试执行键为"key"对应的数据驱动生成的用例

全局数据驱动测试

数据驱动用例需要我们去修改测试用例,并为每个测试用例都增加修饰器和通过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生成数据驱动测试用例。

执行测试

本节主要介绍如果批量执行所有的测试用例并生成对应的测试报告。

批量执行测试用例

当测试用例编写完成,大部分情况下,我们需要全量来执行我们的测试用例,这里我们就可以使用manage.py工具:

$ python manage.py runtest zootest.cat.feed

这种方式执行的所有输出都会打印到stdout。

指定测试报告类型

可以通过指定 -o xml将执行的结果输出到xml文件,命令行为:

$ python manage.py runtest -o xml zootest.cat.feed

执行完成后,可以看到当前目录下生成以下文件:

2016/02/03  08:59               450 zootest.cat.feed.FeedFishTest.xml
2016/02/03  08:59               420 zootest.cat.feed.FeedAppleTest.xml
2016/02/03  08:59               412 zootest.cat.feed.FeedAppleTest.xml
2016/02/03  08:59               709 TestReport.xml
2016/02/03  08:59             4,646 TestReport.xsl
2016/02/03  08:59             5,697 TestResult.xsl

直接双击TestReport.xml用浏览器打开就可以看到测试报告。

如果想要生成在线报告,可以指定-o online,比如:

$ python manage.py runtest -o online zootest.cat.feed

-o online在执行完成后,会发送报告邮件,默认是发给当前登录系统的用户,如果要自行指定,可以联合-n选项,例如:

$ python manage.py runtest -o online -n "guyingzhao;t_QTA;yboxrunner"

还可以通过--reporttype选项控制在线测试报告的类型,可以是任意的字符串,默认是“个人测试”,比如要生成一个性能报告:

$ python manage.py runtest -o online --reporttype 性能测试 zootest.cat.feed

执行后输出一个在线报告的链接,例如:

http://www.qta.com/report/9273512

也可以修改为流输出式的测试报告,这个就是不指定-o选项时,采取的方式,全部输出到stdout:

$ python manage.py runtest -o stream zootest.cat.feed

执行后输出:

Test Cases runs at:2016-02-03 09:02:48.
filter 0 testcases
load 3 testcases
run test case: zootest.cat.feed.FeedFishTest(pass?:True)
run test case: zootest.cat.feed.FeedMouseTest(pass?:True)
run test case: zootest.cat.feed.FeedAppleTest(pass?:True)
Test Cases ends at:2016-02-03 09:02:48.
Total execution time is :0:00:00

并发执行

当测试比较多时,可以考虑通过并发的方式执行测试用例。testrunner内置两种并发方式:多线程和多进程。

使用多线程的方式并发:

$ python manage.py runtest -o stream -x threading -j 2 zootest.cat.feed

这里我们使用2个线程并发去执行全部用例。也可以使用多进程并发:

$ python manage.py runtest -o stream -x multiprocessing -j 2 zootest.cat.feed

指定测试用例

当只需要指定部分用例进行执行时,testrunner也支持选择过滤测试用例。

通过用例名指定测试用例,比如指定多个用例模块:

$ python manage.py runtest -o stream zootest.cat zootest.dog

测试用例名可以是一个包、模块或者类的名字,其参数TestLoader接受的参数是一样的。

也可以通过用例优先级过滤,比如只执行优先级为High和BVT的用例:

$ python manage.py runtest -o stream -p High/BVT zootest

也可以通过用例状态进行过滤,比如只执行Ready状态的用例:

$ python manage.py runtest -o stream -s Ready zootest

自定义测试执行

上面的执行都是通过manage.py来测试用例,如果需要扩展测试用例执行的过程,增加一些测试步骤,也可以通过QTA的接口来执行测试用例。

等价于上面的runtest命令的代码的实现:

from testbase.runner import TestRunner
from testbase.report import XMLTestReport

report = XMLTestReport()
runner = TestRunner(report)
runner.run("zootest.cat.feed")

如果要多线程执行:

runner = ThreadingTestRunner(report)

如果要使用Stream报告:

report = StreamTestReport()

通过使用不同类型的TestRunner和TestReport,可以定制测试执行的方式或测试报告的格式,更多的类型和使用方法,请参考接口文档《testbase.runner Package》和《testbase.report Package

配置测试项目

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

接口文档

接口文档

testbase.conf Package

testbase.context Package

testbase.datadrive Package

testbase.exlib Package

testbase.loader Package

testbase.logger Package

testbase.management Package

testbase.project Package

testbase.report Package

testbase.runner Package

testbase.serialization Package

testbase.testcase Package

testbase.testresult Package

testbase.util Package

配置项说明文档

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