Skip to content

21需求多变,测试框架如何动态挑选用例?

上一讲,我们学习了当测试环境变化时,测试框架如何实现测试环境秒切换。在实现这个功能的同时,我也实现了测试用例查找、测试模块自动导入、测试数据加载,以及测试用例执行等动作。这一讲,我们就来学习如何实现测试用例动态挑选。

在正式开始本节课前,请你在命令行中执行如下命令:

python
pip install iTesting

安装好后,在命令行执行如下命令:

python
iTesting - env dev -k test_demo -m myMark

观察程序的输出值,然后更改命令执行:

java
iTesting --env qa -k login -m myMark

再次观察程序的输出值,看看两者之间有什么不同。

本讲我将带你实现一个可以自由切换测试环境,自主挑选测试用例的测试框架。

动态挑选测试用例的应用场景

测试用例动态挑选的应用场景很多,常见的有如下几种:

  • 开发更改了某个模块的代码,仅需要回归这个模块的测试用例时;

  • 开发人员 Merge 代码到 Develop 分支时,标记了冒烟测试标签的测试用例需要被触发;

  • 在生产环境执行测试时,敏感的测试用例(例如涉及金钱)需要被忽略执行;

  • 当测试框架需要对有特定标签的测试用例执行额外的操作时。

pytest 中实现测试用例动态挑选的方法

在 pytest 框架中,我们知道测试用例的挑选可以有很多方式,我们来回顾下它们的用法:

python
# 1. 按照测试类执行
# 运行test_lagou.py文件下的,类名是TestLaGou下的所有测试用例
pytest test_lagou.py::TestLaGou
# 2. 按照测试方法执行
# 运行test_lagou.py文件下的,类名是TestLaGou下的,名字为test_get_new_message的测试用例
pytest test_lagou.py::TestLaGou::test_get_new_message
# 3. 使用-m 标签 
# 选中同时带有mark1和mark2这个标签的测试用例运行
pytest -m "mark1 and mark2"
# 选中带有mark1的测试用例,不运行mark2的测试用例
pytest -m "mark1 and not mark2" 
# 选中带有mark1或 mark2标签的所有测试用例
pytest -m "mark1 or mark
# 4. 使用-k标签
# 按照文件名称匹配。运行test_lagou.py下的所有的测试
pytest -k "test_lagou.py"
# 按照文件名字部分匹配。只有运行测试文件名字中含有lagou字样,则它含有的所有测试用例都会执行
pytest -k "lagou"

使用上述用法,可以根据用户的需要,仅运行满足条件的测试用例。

如果让你来实现这些功能,你该如何做呢?

自研框架如何实现测试用例动态挑选

下面,我仍以上一讲中实现的 iTesting 这个框架为例,为你详细讲解如何实现这些功能。

首先,查看项目结构:

更改 tests 目录下 test_iTesting.py 这个文件:

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

class Demo:
    @staticmethod
    def login(username, password):
        # 这里写你的业务逻辑,简单起见,我返回True
        print('\n%s' % username)
        print('\n%s' % password)
        return True

class TestDemo:
    def test_login(self, username, password, expected):
        assert Demo.login(username, password) == expected

    def test_demo1(self):
        assert True

    def test_demo2(self):
        assert False

在这个文件中,我新增加了两个测试方法 test_demo1 和 test_demo2。

1.实现类似 pytest 的 -k 功能

现在,我来实现类似 pytest 的 -k 功能,步骤如下:

  • 添加命令行参数 -k;

  • 更改 main.py 中的 run 方法,在发现测试文件、测试类、测试函数时,与 -k 提供的参数值对比,如果 -k 提供的参数是测试文件名、测试类名、测试函数名的子集时,执行这个测试。

(1)第一步, 添加命令行参数 -k。更改 main.py 文件的 parse_options 方法,增加 -k 参数。

python
def parse_options(user_options=None):
    parser = argparse.ArgumentParser(prog='iTesting',
                                     usage='Demo Automation Framework, Search wechat account iTesting for more information')
    parser.add_argument("-env", default='dev', type=str, choices=['dev', 'qa', 'staging', 'prod'], help="Env parameter")
    parser.add_argument("-k", default=None, action="store", help="only run tests which match the given substring expression")
    if not user_options:
        args = sys.argv[1:]
    else:
        args = shlex.split(user_options)
    options, un_known = parser.parse_known_args(args)
    if options.env:
        print("\n想了解更多测试框架内容吗?请关注公众号iTesting")
        print('Currently the env are set to: %s' % options.env)

    if options.k:
        print("你设置了-k参数,将会运行所有包括'%s'的测试文件,测试类,测试函数" % options.k)
    return options

parse_options 这个方法,是利用了 argparse 这个标准库,接收命令行参数并解析。在这里添加了 -k 参数,它没有默认值。

(2)接着我们看第二步,更改 main.py 里的 run 方法:

由于代码层次较多,贴出来的代码不具备可读性,为了讲解方便,我直接贴出 run 方法源码的截图。

为尽可能简洁地向你展示代码原理,并让你看得懂,我选择直接展示最原始、没有优化的代码。在实际工作中,我们应该遵循本课程前面章节中讲的测试框架的设计原则,尽可能抽象共用模块。

仔细观察上述代码,上述代码实现了测试用例按照名称挑选运行,请重点关注以下几行。

  • 第 85 行,首先判断命令行参数 -k 存在不存在。

如参数不存在,直接执行 121 行及以后的代码逻辑,即执行所有测试用例;如果存在,代码走第 87 行。

  • 第 87 行,找出测试 Module(即.py 测试文件)名中含有 -k 参数值的所有测试 module。

如果测试 Module 中包括 -k 的参数值,那么整个 Module 下的所有测试用例都会执行;如果不包括,则代码走第 98 行。

  • 第 98 行,如果测试 Module 中不包括 -k 的参数值,则找出测试类名中含有 -k 的参数值的所有测试类。

如果测试类名中含有 -k 的参数值(第 103 行),则这个测试类下的所有测试用例都会被执行;如果不存在,则代码走 111 行。

  • 第 111 行,找出测试函数名中含有 -k 参数值的所有测试函数。

如果测试函数名中含有 -k 参数值(第 116 行),则执行这个测试函数;反之,则不执行这个测试函数。

仔细观察,你会发现其实我没有写执行模块,即当前我们仅仅查找出了应该运行的测试文件、测试类和测试方法。但我并没有真正运行它,只是打印出该运行的函数名称。

(3)下面我在命令行中分别根据需要来运行下我的程序,看看结果有什么不同。

  • 按照测试文件名挑选测试用例执行
python
iTesting -k 'iTesting'

运行结果如下:

可以看到,test_iTesting.py 下的所有测试用例都被执行了。

  • 按照测试类名挑选测试用例执行
python
iTesting -env dev -k TestDem

运行结果如下:

可以看到,TestDem 匹配到了 TestDemo 这个测试类,所以测试类 TestDemo 下的所有测试用例都被执行了。

  • 按照测试函数名挑选测试用例执行
java
iTesting -env dev -k login

运行结果如下:

可以看到,login 匹配到了如下目录 (test_iTesting/TestDemo/) 下的 test_login 函数,所以 test_login 被执行了。

通过给定 -k 参数,我们实现了根据名字模糊匹配并执行测试用例。

2.实现类似 pytest 的 -m 功能

现在,我来实现类似 pytest 的 -m 功能,步骤如下:

  • 添加 -m 这个命令行参数;

  • 给所有测试类,测试函数打标签;

  • 当通过 -m 给定的标签等于测试类/测试函数的标签时,执行这个测试类/测试函数。

(1)第一步,添加命令行参数 -m。

更改 main.py 文件的 parse_options 方法,增加 -m 参数:

python
def parse_options(user_options=None):
    parser = argparse.ArgumentParser(prog='iTesting',
                                     usage='Demo Automation Framework, Search wechat account iTesting for more information')
    parser.add_argument("-env", default='dev', type=str, choices=['dev', 'qa', 'staging', 'prod'], help="Env parameter")
    parser.add_argument("-k", default=None, action="store", help="only run tests which match the given substring expression")
    parser.add_argument("-m", default=None, action="store", help="only run tests with same marks")
    if not user_options:
        args = sys.argv[1:]
    else:
        args = shlex.split(user_options)
    options, un_known = parser.parse_known_args(args)
    if options.env:
        print("\n想了解更多测试框架内容吗?请关注公众号iTesting")
        print('Currently the env are set to: %s' % options.env)
    if options.k:
        print("你设置了-k参数,将会运行所有包括'%s'的测试文件,测试类,测试函数" % options.k)
    if options.m:
        print("你设置了-m参数,将会运行所有标签为'%s'的测试类,测试函数" % options.m)

    return options

(2)第二步,给所有测试类和测试方法类打标签,这就要用到装饰器。

注意,这里我为了使得代码量尽量少,演示尽量简单,先行定义以下打标签要遵循的规则:

当前仅实现给测试方法打标签,给测试类打标签暂不实现。

在 main.py 文件中定义一个新函数:

python
# 确保以下第二行放入main.py开头的导入语句
from functools import wraps
# 新增一个类装饰器用来装饰测试用例
class TestMark(object):
    def __init__(self, mark=None):
        self.mark = mark
    def __call__(self, func):
        @wraps(func)
        def wrapper():
            return func
        setattr(wrapper, "__test_case_mark__", self.mark)
        return wrapper

接着,更改 test_iTesting.py 文件,给测试类 TestDemo 下的测试方法加上装饰器 TestMark:

python
# 此文件中,其他部分不变,只更改如下部分
class TestDemo:
    def test_login(self, username, password, expected):
        assert Demo.login(username, password) == expected
    @TestMark('myMark')
    def test_demo_true(self):
        assert True
    def test_demo_false(self):
        assert False

最后,把识别 -m 的逻辑添加到 run 方法。为了演示方便,我这里展示出,当没有指定 -k 参数时,判断 -m 标签的源码:

请观察:

  • 第 142 行,判断测试函数有没有 -m 参数。没有 -m 参数,执行第 148 行及以后的代码,执行完就结束;有 -m 参数,继续第 2 步。

  • 有 -m 的情况下,查看测试函数的"test_cse_mark"这个属性,并且要判断它的值等不等于用户传入的 -m 的参数值(第 143 行)。如果都相同,则将执行这个测试函数;如果不相同,则不执行这个测试函数。

注意:关于 -m 的判断,是在最后 func_name.startswith('test') 这里判断的(这个语句在上述的 141 行),这是个独立的判断,即所有存在 func_name.startswith('test') 这句话后,都应该加 -m 的判断。明白了这个之后,我们把其余存在 func_name.startswith('test') 这句话的地方进行更改,更改后的 run 方法如下所示。

现在在 -k 参数存在的情况下,将 -m 参数的代码补上:

在 -k 存在的情形下,添加 -m 方法很简单,找到func_name.startswith('test')然后执行替换操作即可。

(3)下面我们来运行下,看看效果如何。

  • 不指定 -k,只指定 -m。
python
iTesting -m myMark

运行结束后,查看结果如下:

因为只有 test_demo_true 这个函数有 myMark 的标签,所以只有它执行了。

  • 指定 -k,匹配到测试 Module,不指定 -m。
python
iTesting -k iTesting

运行结束后,查看结果如下:

因为匹配到了 test_iTesting.py,所以 test_iTesting.py 下面所有测试用例都被执行了。

  • 指定 -k,匹配到测试类TestDemo,指定 -m。
python
iTesting -k TestDemo -m myMark

运行结束后,查看结果如下:

虽然 TestDemo 匹配了 test_iTesting.py 文件夹下的两个函数 test_demo_true 和 test_demo_false。但是因为只有 test_demo_true 用 myMark 这个标签,故只有它被运行了。

  • 指定 -k,匹配到测试函数 test_login,指定 -m。
python
iTesting -k login -m myMark

运行结束后,查看结果如下:

可以看到虽然 -k 和 -m 分别匹配到了 test_login 和 test_demo_true 两个函数,但是没有一个函数是同时满足 -k 和 -m 的条件的,所以没有测试用例被执行。

总结

这一讲,我们详细讨论并解释了如何动态挑选测试用例。从 pytest 动态挑选测试用例的方法入手,我以 -k 和 -m 两个参数为例,逐一编码实现了与 pytest 类似的功能。本节课内容较多,代码层次也更深,希望你能实际动手操作一遍以加深理解。

至此,我已经带领大家实现了自研框架的大部分功能,包括测试用例查找、测试模块自动导入、测试数据加载、测试用例执行,以及测试用例按照名称或者标签挑选执行。当然,为了尽可能清晰简洁地向你讲解代码实现的原理,我们本讲的代码采用流水线的方式,不具备美感。

在实际工作中,真正可用的自研框架,其代码一定比这个复杂得多,而且更抽象,解耦也会做得比这个好。所以本讲希望你以了解其原理及实现过程为主,勤加练习。

关于自研框架更深入层次的技术介绍,请关注我的公众号 iTesting 并回复"测试框架"查看。


课程评价入口,挑选 5 名小伙伴赠送小礼品~