iTesting软件测试知识分享

如何高效组织自动化测试用例

自动化测试用例写多了,不可避免会遇到这个问题,每次运行无需运行所有的用例,那么如何把要运行的用例挑出来并高效组织它们呢?
一般说来,通用的做法都是

把要运行的用例用特殊标记Mark出来,然后框架运行时,自动寻找这些带标记的case,并把它们装到一个新的test suite里。
所以高效组织测试用例的关键就是两部分:

1. 如何标记待测试用例.
2. 运行时如何收集这些带标记的用例。

以下列出来我经历过的方法:

1.给所有的测试用例编号,把测试用例按照 编号, summary, Path, ClassName, 测试运行与否的flag等等,写到一个外部文件里,通常是Excel,

每次测试框架运行时,先去这个文件里,一行一行的读文件,如果发现“测试运行与否的flag”的值是True, 就把这条case拿出来放到一个临时变量里,读完整个文件,就拿到了
所有要运行的case, 这个时候,导入这个case需要的所有依赖,运行这个case,并把运行结果写回到一个results的文件里。 那么,改动老的case,增加新的case,怎么办呢?
勤快一点的,每写一个/更改一个自动化用例,手工更新这个外部表格。 懒一点的,写一段代码,每写/改一个case,手工运行这段代码,把更改写进去。

2.利用装饰器,经典的如TestNG,可以在测试用例前加@BeforeSuite, 今天要讲的也是这个。如何在python里实现。

先说下思想: 我期望自动化测试运行时候,可以根据我提供的不同的tag,运行不同的测试用例。 前提是我写自动化用例的时需给每个测试用例,都加上tag并赋值。
在运行时候,框架通过我提供的不同的tag,去找这些被标记的case,然后组织起来运行。

要实现这个,我们需要接收用户参数,可以用:

1.sys.argv[1:]来自己写代码处理,也可以用标准库argparse来,我们用后者。

argparse 的用法很直观, 先看段代码::

1
2
3
4
5
6
7
8
9
10
1. 先创建一个parser:
parser = argparse.ArgumentParser(prog='para')
2. 加入一些参数:
parser.add_argument('-w', action='store', default='.', help='specify the work space')
parser.add_argument('-target', action='store', help='specify the run target file')
parser.add_argument('-tag', action='store', help='run by tag')
parser.add_argument('-n', action='store', help='concurrent user')
3. Parseing 这些参数
options = parser.parse_args()
print (options)

这个时候,如在command line里输入

1
python __init__.py -target test -tag smoke -n 3
就会得到如下结果:
1
2
Namespace(n='3', tag='smoke', target='test', w='.')

使用argparse就可以方便的获取用户参数,并处理。关于argparse的用法,请参考官网。

2.用户输入的参数我们接收处理了,那么如何处理这些tag呢,这就要用到装饰器:
1
2
3
4
5
6
7
8
9
10
11
12
def TestCase(enabled=True, tags=None, **kwargs):
def tracer(cls):
cls.__type__ = 'Test'
cls.__enabled__ = enabled
if not tags:
cls.__tags__ = None
else:
# 想想看,如果tag有好几个,这块代码应该怎么改
cls.__tags__ = tags
return cls
return tracer

上面的代码呢,我们定义了一个装饰器,这个装饰器唯一的作用,是接收传入的类,然后给这些类增加一些属性,这些属性后续用来判断是否要执行。 使用时在新建立测试类上加上@Testcase即可。

3.测试用例类,这些测试类或者测试类中的函数实现了一个个的功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class RealTest():
@TestCase(tags='smoke')
def test1(self):
logging.info("this is test 1")
time.sleep(5)
@TestCase(enabled=True, tags='smoke')
def test2(self):
logging.info("this is test2")
time.sleep(5)
@TestCase(enabled=False, tags='smoke')
def test3(self):
logging.info("this is test3")
time.sleep(5)

我们可以利用装饰器里的tag参数, enable参数来实现测试用例的筛选。

4.我们再定义一个类,这个类作为一个test case的执行器,把要运行的测试用例装进来,并发,或者顺序执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TestExecutor(threading.Thread):
def __init__(self, cls, worker):
threading.Thread.__init__(self)
self.cls = cls
self.workers = worker
self._lock = threading.RLock()
def run(self):
self.workers.acquire()
logging.info("Thread {threadname}".format(threadname=self.current_executor()))
self.cls(self)
self.workers.release()
def current_executor(self):
return threading.current_thread()
5.利用threading.Semaphore实现线程并发限制。
6.实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import argparse
import threading
import time
import logging
logging.basicConfig(level=logging.INFO , format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
def TestCase(enabled=True, tags=None, **kwargs):
def tracer(cls):
cls.__type__ = 'Test'
cls.__enabled__ = enabled
if not tags:
cls.__tags__ = None
else:
# 想想看,如果tag有好几个,这块代码应该怎么改
cls.__tags__ = tags
return cls
return tracer
class TestExecutor(threading.Thread):
def __init__(self, cls, worker):
threading.Thread.__init__(self)
self.cls = cls
self.workers = worker
self._lock = threading.RLock()
def run(self):
self.workers.acquire()
logging.info("Thread {threadname}".format(threadname=self.current_executor()))
self.cls(self)
self.workers.release()
def current_executor(self):
return threading.current_thread()
class RealTest():
@TestCase(tags='smoke')
def test1(self):
logging.info("this is test 1")
time.sleep(5)
@TestCase(enabled=True, tags='smoke')
def test2(self):
logging.info("this is test2")
time.sleep(5)
@TestCase(enabled=True, tags='smoke')
def test3(self):
logging.info("this is test3")
time.sleep(5)
if __name__ == "__main__":
s = threading.Semaphore(2)
parser = argparse.ArgumentParser(prog='para')
parser.add_argument('-w', action='store', default='.', help='specify the work space')
parser.add_argument('-target', action='store', help='specify the run target file')
parser.add_argument('-tag', action='store', help='run by tag')
parser.add_argument('-n', action='store', help='concurrent user')
options = parser.parse_args()
real_cases = []
for module_element in dir(RealTest):
pre_test_class = getattr(RealTest,module_element) # @Mark
if options.tag:
if hasattr(pre_test_class, "__type__") and \
hasattr(pre_test_class, "__enabled__") \
and pre_test_class.__enabled__\
and hasattr(pre_test_class, "__tags__")\
and pre_test_class.__tags__ \
and pre_test_class.__tags__.lower() == options.tag.lower():
real_cases.append(pre_test_class)
threads = []
for item in real_cases:
threads.append(TestExecutor(item, s))
for t in threads:
t.start()
for t in threads:
t.join()

这里重点讲下标记为

1
#@Mark
的语句,实际上这个就是如何把标记的case挑选出来的核心方法。
我们使用了反射机制

反射: 有时候我们会碰到这样的需求,需要执行对象的某个方法,或是需要对对象的某个字段赋值,而方法名或是字段名在编码代码时并不能确定,需要通过参数传递字符串的形式输入。举个具体的例子:当我们需要实现一个通用的DBM框架时,可能需要对数据对象的字段赋值,但我们无法预知用到这个框架的数据对象都有些什么字段,换言之,我们在写框架的时候需要通过某种机制访问未知的属性。这个机制被称为反射(反过来让对象告诉我们他是什么),或是自省

dir([obj]):
调用这个方法将返回包含obj大多数属性名的列表(会有一些特殊的属性不包含在内)。obj的默认值是当前的模块对象。

hasattr(obj, attr):
这个方法用于检查obj是否有一个名为attr的值的属性,返回一个布尔值。

getattr(obj, attr):
调用这个方法将返回obj中名为attr值的属性的值,例如如果attr为’bar’,则返回obj.bar。

对于所有的测试类,我们会逐个把他们导入(示例里无这段代码,关于case class的导入可以根据 -target这个参数的值来代码实现,此处未展示。)
对于每一个类,我们通过dir(), getattr()拿到测试类所有的可用方法,然后这些方法因为有装饰器TestCase的加持,我们可以顺利的根据参数-tags拿到要运行的测试用例。
然后我们根据拿到的每一个测试用例,判断它们是否有需要的属性( enabledtype), 然后对于每个有这个属性的用例,我们在放到真正运行的测试用例list里。
最后对测试用例list的用例,每一个都放入TestExecutor里执行,TestExecutor 实现了多线程及多线程限制。

我们来看下运行效果:
输入:
python Android/init.py -target test -tag smoke -n 3
输出:
因为我们限制了并发线程数为2, 所以我们先看到以下输出。

1
2
3
4
5
Namespace(n='3', tag='smoke', target='test', w='.')
2017-07-07 20:01:25,009 - __init__.py[line:31] - INFO: Thread <TestExecutor(Thread-1, started 11284)>
2017-07-07 20:01:25,009 - __init__.py[line:42] - INFO: this is test 1
2017-07-07 20:01:25,009 - __init__.py[line:31] - INFO: Thread <TestExecutor(Thread-2, started 10196)>
2017-07-07 20:01:25,010 - __init__.py[line:47] - INFO: this is test2

5秒后,又看到如下:
1
2
2017-07-07 20:01:30,010 - __init__.py[line:31] - INFO: Thread <TestExecutor(Thread-3, started 14156)>
2017-07-07 20:01:30,011 - __init__.py[line:52] - INFO: this is test3

如此一来,我们仅需要写测试用例时,添加合适的tag,然后运行时用参数区分,就可以不修改代码的的情况下运行不同的case了。

🐶 您的支持将鼓励我继续创作 🐶
-------------评论, 吐槽, 学习交流,请关注微信公众号 iTesting-------------
请关注微信公众号 iTesting wechat
扫码关注,跟作者互动