Appearance
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 并回复"测试框架"查看。