Tempest测试类型解析:满屏装饰器

刚刚开始看openstack或者tempest的人,会发现都是装饰器,所以要抱着激情的态度来迎合它,而不是抵触,相信多看个几次就会慢慢习惯了

还是先看前面老是提起的flavor这个case

@attr(type='smoke')
def test_list_flavors(self):
# List of all flavors should contain the expected flavor
resp, flavors = self.client.list_flavors()
resp, flavor = self.client.get_flavor_details(self.flavor_ref)
flavor_min_detail = {'id': flavor['id'], 'links': flavor['links'],
'name': flavor['name']}
self.assertIn(flavor_min_detail, flavors)

这个case整个测试流程,前面一篇已经写过了,但是现在关心的是成员函数上面的哪一行,很显然这里是一个装饰器,而且带了一个辨识度很高的参数:type=‘smoke’,可能猜也能猜到是类型,因为后面还有type=‘gate’,抱着好奇的态度看下

首先是装饰器attr

def attr(*args, **kwargs):
"""A decorator which applies the nose and testtools attr decorator

This decorator applies the nose attr decorator as well as the
the testtools.testcase.attr if it is in the list of attributes
to testtools we want to apply.
"""

def decorator(f):
if 'type' in kwargs and isinstance(kwargs['type'], str):
f = testtools.testcase.attr(kwargs['type'])(f)
if kwargs['type'] == 'smoke':
f = testtools.testcase.attr('gate')(f)
elif 'type' in kwargs and isinstance(kwargs['type'], list):
for attr in kwargs['type']:
f = testtools.testcase.attr(attr)(f)
if attr == 'smoke':
f = testtools.testcase.attr('gate')(f)
return nose.plugins.attrib.attr(*args, **kwargs)(f)

return decorator

传入参数type=‘smoke’,因此args为空,而kwargs[‘type’]为smoke,是一个str,因此进入if当中,接着看下testtbools.testcase里的attr

不出意外,又是一个装饰器

def attr(*args):
"""Decorator for adding attributes to WithAttributes.

:param args: The name of attributes to add.
:return: A callable that when applied to a WithAttributes will
alter its id to enumerate the added attributes.
"""
def decorate(fn):
if not safe_hasattr(fn, '__testtools_attrs'):
fn.__testtools_attrs = set()
fn.__testtools_attrs.update(args)
return fn
return decorate

这里传入了参数smoke,先看下safe_hasattr

def safe_hasattr(obj, attr, _marker=object()):
"""Does 'obj' have an attribute 'attr'?

Use this rather than built-in hasattr, as the built-in swallows exceptions
in some versions of Python and behaves unpredictably with respect to
properties.
"""
return getattr(obj, attr, _marker) is not _marker

继续看getattr

def getattr(object, name, default=None): # known special case of getattr
"""
getattr(object, name[, default]) -> value

Get a named attribute from an object; getattr(x, 'y') is equivalent to x.y.
When a default argument is given, it is returned when the attribute doesn't
exist; without it, an exception is raised in that case.
"""
pass

内联函数,得到object.name,也就是上面一个函数obj.attr,而返回的就是第三个参数,因此safe_hasattr的意思就是:

如果obj没有attr这个成员变量,is not为真,函数返回true;如果有这个成员变量,那么obj.attr=_marker_ is not _marker_就是false

因此回到装饰器里,意思就是如果fn没有__testtools_attrs这个成员变量,强行赋值一个元组,然后再将smoke给添加进去;如果有这个成员变量,就直接添加smoke,至于__testtools_attrs是啥,不明,没搜到,猜测可能是testtools这个测试工具里的一个啥全局变量

因此下面这行的目的只是给f更新一个smoke属性,其实猜也能猜到可能就是为了最终测试通过某个参数来决定只执行smoke test

f = testtools.testcase.attr(kwargs['type'])(f)

继续往下,如果传入的type是smoke,f再次更新一下属性,添加了一个gate类型属性,说实话这里没看明白想干嘛,不过后面还有一个nose框架的执行,猜测这里是testtools的执行方式,else就不看了

return nose.plugins.attrib.attr(*args, **kwargs)(f)

到了return,看下nose里的attr,肯定还是装饰器

def attr(*args, **kwargs):
"""Decorator that adds attributes to classes or functions
for use with the Attribute (-a) plugin.
"""
def wrap_ob(ob):
for name in args:
setattr(ob, name, True)
for name, value in kwargs.iteritems():
setattr(ob, name, value)
return ob
return wrap_ob

这里传入的参数,args为空,kwargs是type=‘smoke’,因此第一个for没啥动静,第二个for执行setattr,看命名应该就能猜到是给ob设置一个属性smoke,看下具体代码

def setattr(p_object, name, value): # real signature unknown; restored from __doc__
"""
setattr(object, name, value)

Set a named attribute on an object; setattr(x, 'y', v) is equivalent to
``x.y = v''.
"""
pass

不出所料,实际上nose那行还是给f设置一个type属性,为smoke,可以说成是f.type=smoke

综上所述,需要给测试的case设置一种类型的属性,就可以在前面添加一个这种装饰器来进行区分

下面可以做一个简单的测试,test_flavors.py里面只有三个smoke类型的test case

$ nosetests -sv test_flavors.py -a type='smoke'
tempest.api.compute.flavors.test_flavors.FlavorsTestJSON.test_get_flavor ... ok
tempest.api.compute.flavors.test_flavors.FlavorsTestJSON.test_list_flavors ... ok
tempest.api.compute.flavors.test_flavors.FlavorsTestJSON.test_list_flavors_with_detail ... ok

----------------------------------------------------------------------
Ran 3 tests in 3.812s

OK

这里我手动添加一个简单的case

@attr(type='smoke')
def test_lihui(self):
assert 1 == 1

执行一下,有多执行一个case

$ nosetests -sv test_flavors.py -a type='smoke'
tempest.api.compute.flavors.test_flavors.FlavorsTestJSON.test_get_flavor ... ok
tempest.api.compute.flavors.test_flavors.FlavorsTestJSON.test_lihui ... ok
tempest.api.compute.flavors.test_flavors.FlavorsTestJSON.test_list_flavors ... ok
tempest.api.compute.flavors.test_flavors.FlavorsTestJSON.test_list_flavors_with_detail ... ok

----------------------------------------------------------------------
Ran 4 tests in 3.590s

OK

弄一个错误的看看

$ nosetests -sv test_flavors.py -a type='smoke'
tempest.api.compute.flavors.test_flavors.FlavorsTestJSON.test_get_flavor ... ok
tempest.api.compute.flavors.test_flavors.FlavorsTestJSON.test_lihui ... FAIL
tempest.api.compute.flavors.test_flavors.FlavorsTestJSON.test_list_flavors ... ok
tempest.api.compute.flavors.test_flavors.FlavorsTestJSON.test_list_flavors_with_detail ... ok

======================================================================
FAIL: tempest.api.compute.flavors.test_flavors.FlavorsTestJSON.test_lihui
----------------------------------------------------------------------
_StringException: Empty attachments:
pythonlogging:''

Traceback (most recent call last):
File "/Users/lihui/work/openstack/tempest/api/compute/flavors/test_flavors.py", line 32, in test_lihui
assert 1 == 2
AssertionError


----------------------------------------------------------------------
Ran 4 tests in 6.102s

FAILED (failures=1)

发表回复