Openstack中的测试 ( by quqi99 )
在openstack中写一个扩展API之后要写哪些测试代码呢?https://jenkins.openstack.org/view/Nova/?列出来了提交代码后jenkins所要做的测试。
1)通过Mock带隔离的测试,包括单元测试,还有针对API和example的功能测试(在Mock环境中针对一个个具体的API和example做测试)
2)不带隔离的真实环境的测试,包括functional test(尽量在真实环境中辅以少量的mock串起来测试,neutron中的代码位于:$neutron/tests/functional, 运行时添加OS_SUDO_TESTING=True),和集成测试(在真实环境中将多个API串起来测试, 代码位于tempest工程)。
至于如何写扩展API (位于目录$nova/nova/api/openstack/compute/contrib),请参见我的上一篇博文(Openstack extension api ( by quqi99 ) http://blog.csdn.net/quqi99/article/details/8502034) .
1, Mock单元测试, (位于目录$nova/nova/tests/api/openstack/compute/contrib) 可以参见文档:http://wiki.openstack.org/SmallTestingGuide
Mock单元测试,就是经常说的Small Testing, 它强调隔离,也就是说我们只将精力集中在我们要测试的方法内,如果该方法调用了其他方法,都可以让它做Mock即返回一些假值。
代码隔离有两种方法:
1)一种是依赖注入,如下例子,想要测试FalilyTree类的话,应集中精力测试FalilyTree本身是否有错,至于它所依赖的PersonGateway可以做一个假的FakePersonGateway注入进去。
class FamilyTree(object):
def __init__(self, person_gateway):
self._person_gateway = person_gateway
person_gateway = FakePersonGateway()
# ...
tree = FamilyTree(person_gateway)
2) Monkey Patching, 如下例子,例用python脚本语言的特性,在运行时可以动态替换命名空间的方法,用FakePersonGateway替换掉命名空间mylibrary.dataaccess.PersonGateway
class FamilyTree(object):
def __init__(self):
self._person_gateway = mylibrary.dataaccess.PersonGateway()
mylibrary.dataaccess.PersonGateway = FakePersonGateway
# ...
tree = FamilyTree()
所以,相应地,也就有了下列几类实际隔离的方法:
1) Test Stub,打桩
比如在扩展API中调用了authorize这个函数,如下:
authorize = extensions.extension_authorizer('compute', 'flavor_dynamic')
class FlavorDynamicController(servers.Controller):
@wsgi.extends
def create(self, req, body):
context = req.environ['nova.context']
authorize(context)
那么在为它写单元测试时,对authorize函数打桩替换的代码如下:
def authorize(context):
pass
class FlavorDynamicTest(test.TestCase):
def setUp(self):
super(FlavorDynamicTest, self).setUp()
self.controller = flavor_dynamic.FlavorDynamicController()
self.stubs.Set(flavor_dynamic, 'authorize', authorize)
2) Mock对象,例如要测试的方法中调用了
inst_type = instance_types. \
get_instance_type_by_name(flavor_name, context)
现在可以用下面的方法将其替换:
self.mox.StubOutWithMock(flavor_dynamic.instance_types,
'get_instance_type_by_name')
flavor_dynamic.instance_types.get_instance_type_by_name(
mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(None)
flavor_dynamic.instance_types.get_instance_type_by_name(
mox.IgnoreArg(), mox.IgnoreArg()).AndReturn("{'a'='1'")
self.mox.ReplayAll()
self.mox.VerifyAll() #用来验证上面录制的mox重放的次数是否与实际运行的相同。
所以在考虑是否重构已有的单元测试代码时,可以考虑:
1)是否使用了VerifyAll
2)测试父盖率,可通过工具查看
上面是用mox模板实现的Mock对象,还可以用mock模块来实现,上面是一个个方法Mock,这里可以对一个类的所有方法Mock,如:
self.utils_exec_p = mock.patch(
'neutron.agent.linux.utils.execute')
self.utils_exec = self.utils_exec_p.start()
self.addCleanup(self.utils_exec_p.stop)
如在测试方法中用到了self.router_info_inst,我们可以将它用Mock替换了, self.router_info_inst = mock.Mock()
self.router_info_inst里还有一个iptables_manager属性,我们继续用Mock替换,
self.iptables_inst = mock.Mock()
self.router_info_inst.iptables_manager = self.iptables_inst
也可以用注解,注意:多个注解时,顺序刚好是反的。
@patch('charmhelpers.contrib.network.ip.config')
@patch('charmhelpers.contrib.network.ip.unit_get')
def test_configure_phy_nic_mtu(self, mock_config, mock_unit_get):
mock_config.config.return_value = 1400
net_ip.configure_phy_nic_mtu()
3) Fake对象,即创建大量的假对象。
2, 除了上面的单元测试,还有针对api和samples的功能测试,位于$nova/nova/tests/integrated/api_samples目录,它们和集成测试的区别在于集成测试采用的是真实环境,而它仍然需要做Mock隔离。
https://blueprints.launchpad.net/nova/+spec/nova-api-samples 介绍了如何写这种测试
https://review.openstack.org/#/c/17003/ 是一个这种测试的具体例子
3, 集成测试,位于tempest工程中,一个好的例子, https://review.openstack.org/#/c/9507/
4, 通过一个side_effect的错误例子举一反三加深对mock的理解,side_effect和一个mock关联,当运行到mock处时,能通过side_effect做另一件期望的事情。下面这个例子是错误的,只是想加深对mock的理解而已。最初的想法是,修改了一个并发的问题,即两个方法应该串行的执行(如Neutron中的IPAllocationPool,要求分配ip与释放ip的方法在多host分布式的情况下也应该串行执行),但是openstack中目前还没有提供分布式锁(一个python进程下的多个线程可通过greenlet来同步,一台host上的多个进程通过oslo提供的文件锁来同步,但对多host的分布式锁目前是没有的,分布式锁可以采用zookeeper实现),只能通过数据库的事务来做,将两个方法都放在长事务中,再使用select for update (如果已经在一个事务里的话,另一个事务运行到select for update时会等待,这就实现了我们要用的串行。但缺点是select for update在有些数据库如postgresql中不能用在左联结中,因为左联结可能产生None值)。
当然也可以使用用串行化事务(session.execute('set transaction isolation level serializable'),当运行到一个事务中时,也能进入到另一个事务的代码中,不会像select for update那样阻塞,但在提交事务时会抛错。
像这样数据库并发的问题,实际上用mock就是很难测的,下面用side_effect只能能模拟并发,但对于数据库并发仍然没法测(因为你不能把那也mock呀),从这个例子中,实际上不需要对这种情况做单元测试,仅仅对单个方法做一个单元测试保证一个测试率,并发的这个问题需采用集成测试。从这个例子,还能看出mock中的side_effect中的一个问题,是mock的那个对象执行了多少次那么side_effect就会执行多少次,所以下面side_effect中的代码实际上是执行很多次的。
这个例子也说明,写mock不难,但写mock得对代码相当熟悉,熟悉到知道在代码的哪个点造假塞mock,所以有时候写mock比写正式代码麻烦多了。
def test_allocate_and_free_ip_emulated_race_condition(self):
def _allocate_ip_from_db(net_id, subnet_id, tenant_id):
allocated_port_fixed_ips = [{'subnet_id': subnet['subnet']['id'],
'ip_address': "10.0.0.226"}]
allocated_port_res = self._create_port(self.fmt,
net_id,
201,
tenant_id=tenant_id,
fixed_ips=allocated_port_fixed_ips,
set_context=True)
allocated_port = self.deserialize(self.fmt, allocated_port_res)
self._delete('ports', allocated_port['port']['id'])
def _release_ip_from_db(net_id, subnet_id, tenant_id):
free_port_fixed_ips = [{'subnet_id': subnet_id,
'ip_address': "10.0.0.225"}]
free_port_res = self._create_port(self.fmt,
net_id,
201,
tenant_id=tenant_id,
fixed_ips=free_port_fixed_ips,
set_context=True)
free_port = self.deserialize(self.fmt, free_port_res)
self._delete('ports', free_port['port']['id'])
with self.subnet(cidr='10.0.0.224/28') as subnet:
net_id = subnet['subnet']['network_id']
subnet_id = subnet['subnet']['id']
tenant_id = subnet['subnet']['tenant_id']
ctx = context.Context('', tenant_id)
with mock.patch.object(context, 'Context') as _mock:
# with mock.patch.object(NeutronManager.get_plugin(), 'create_port') as _mock:
_mock.return_value = ctx
with mock.patch.object(ctx.session, 'query') as side_effect_mock:
def _side_effect(*args, **kwargs):
_release_ip_from_db(net_id, subnet_id, tenant_id)
side_effect_mock.side_effect = _side_effect
_allocate_ip_from_db(net_id, subnet_id, tenant_id)
下面是测试方法:
from sqlalchemy import create_engine
import sqlalchemy.orm
from neutron.db import models_v2
def test_traction():
subnet_id = '189c2c0d-f3a7-4724-9f66-b5d5acef9816'
subnet_id_2 = 'cc9027ed-09e9-43ba-bc5d-152798b21a2f'
engine = create_engine('mysql://root:[email protected]/ovs_neutron?charset=utf8')
Session = sqlalchemy.orm.sessionmaker(bind=engine)
session = Session()
# session.execute('set transaction isolation level serializable')
max_retries = 3
# NOTE(zhhuabj): allocate/free operation about the table IPAvailabilityRange
# must be put into a repeatedly executed transactional bolck
# to ensure they are executed linearly in spite of the
# specified transactions isolation level value.
for i in xrange(max_retries):
# LOG.debug(_('Recycling ip %s'), ip)
try:
with session.begin(subtransactions=True):
pool_qry = session.query(models_v2.IPAllocationPool).with_lockmode('update')
allocation_pool = pool_qry.filter_by(subnet_id=subnet_id).first()
ip_pool = models_v2.IPAllocationPool(subnet_id=subnet_id_2,
first_ip='192.168.1.1', last_ip='192.168.1.4')
session.add(ip_pool)
allocation_pool['first_ip']='10.0.1.3'
print allocation_pool.first_ip
break
except exc.ResourceClosedError, exc.InvalidRequestError:
# a concurrent transaction has been commited, try again
LOG.debug(_('Recycling ip failed due to a concurrent'
'transaction had been commited (%s attempts left)'),
max_retries - (i + 1))
raise q_exc.NeutronException(message='Unable to recycling ip')
if __name__ == '__main__':
test_traction()
例子二,通过assert_has_calls判断哪些call被调用了, 见:https://review.openstack.org/#/c/44345/8/neutron/tests/unit/openvswitch/test_ovs_neutron_agent.py
例如,在下面setup_physical_bridges方法中增加了一行 utils.execute(['/sbin/udevadm', 'settle', '--timeout=10']),
def setup_physical_bridges(self, bridge_mappings):
.....
if ip_lib.device_exists(int_veth_name, self.root_helper):
ip_lib.IPDevice(int_veth_name, self.root_helper).link.delete()
utils.execute(['/sbin/udevadm', 'settle', '--timeout=10'])
int_veth, phys_veth = ip_wrapper.add_veth(int_veth_name, phys_veth_name)
这个不能只测 utils.execute(['/sbin/udevadm', 'settle', '--timeout=10']) 这一句被执行了,而是应该测delete, 这一句, add_veth 这三句按顺序被执行了。
所以我们可以通过attach_mock方法attach到它的父一级的mock上,这样用parent mock的assert_has_calls来看我们想要执行的三句究竟执行了没有。
def test_setup_physical_bridges(self):
with contextlib.nested(
mock.patch.object(ip_lib, "device_exists"),
mock.patch.object(sys, "exit"),
mock.patch.object(utils, "execute"),
mock.patch.object(ovs_lib.OVSBridge, "remove_all_flows"),
mock.patch.object(ovs_lib.OVSBridge, "add_flow"),
mock.patch.object(ovs_lib.OVSBridge, "add_port"),
mock.patch.object(ovs_lib.OVSBridge, "delete_port"),
mock.patch.object(self.agent.int_br, "add_port"),
mock.patch.object(self.agent.int_br, "delete_port"),
mock.patch.object(ip_lib.IPWrapper, "add_veth"),
mock.patch.object(ip_lib.IpLinkCommand, "delete"),
mock.patch.object(ip_lib.IpLinkCommand, "set_up"),
mock.patch.object(ip_lib.IpLinkCommand, "set_mtu")
) as (devex_fn, sysexit_fn, utilsexec_fn, remflows_fn, ovs_addfl_fn,
ovs_addport_fn, ovs_delport_fn, br_addport_fn,
br_delport_fn, addveth_fn, linkdel_fn, linkset_fn, linkmtu_fn):
devex_fn.return_value = True
parent = mock.MagicMock()
parent.attach_mock(utilsexec_fn, 'utils_execute')
parent.attach_mock(linkdel_fn, 'link_delete')
parent.attach_mock(addveth_fn, 'add_veth')
addveth_fn.return_value = (ip_lib.IPDevice("int-br-eth1"),
ip_lib.IPDevice("phy-br-eth1"))
ovs_addport_fn.return_value = "int_ofport"
br_addport_fn.return_value = "phys_veth"
self.agent.setup_physical_bridges({"physnet1": "br-eth"})
expected_calls = [mock.call.link_delete(),
mock.call.utils_execute(['/sbin/udevadm',
'settle']),
mock.call.add_veth('int-br-eth',
'phy-br-eth')]
parent.assert_has_calls(expected_calls, any_order=False)
再举个例子:
在下面代码中增加了一句“port[portbindings.HOST_ID] = host”该如何写单元测试呢?
port['admin_state_up'] = True
port['device_owner'] = 'neutron:' + constants.LOADBALANCER
port['device_id'] = str(uuid.uuid5(uuid.NAMESPACE_DNS, str(host)))
+ port[portbindings.HOST_ID] = host
self.plugin._core_plugin.update_port(
context,
port_id,
{'port': port}
根本思想就是要测这句确实执行过了,而下面的update_port用了它,所以我们可以将update_port做成Mock,然后用mock的assert_called_once_with看它我们给出的加了portbings.HOST_ID:'host'参数exp字典是否是实际执行的port参数的子集,如下:
def test_plug_vip_port_mock_with_host(self):
exp = {
'device_owner': 'neutron:' + constants.LOADBALANCER,
'device_id': 'c596ce11-db30-5c72-8243-15acaae8690f',
'admin_state_up': True,
portbindings.HOST_ID: 'host'
}
with mock.patch.object(
self.plugin._core_plugin, 'update_port') as mock_update_port:
with self.pool() as pool:
with self.vip(pool=pool) as vip:
ctx = context.get_admin_context()
self.callbacks.plug_vip_port(
ctx, port_id=vip['vip']['port_id'], host='host')
mock_update_port.assert_called_once_with(
ctx, vip['vip']['port_id'],
{'port': testlib_api.SubDictMatch(exp)})
class SubDictMatch(object):
def __init__(self, sub_dict):
self.sub_dict = sub_dict
def __eq__(self, super_dict):
return all(item in super_dict.items()
for item in self.sub_dict.items())
3,contextlib.nested has been deprecated in favour of the multiple manager form of the with statement.with-statements now directly support multiple context managers.
Nested context does not work here because a value is modified between the contexts. In addition, 2.6 does not support multiple contexs in one statement.
with contextlib.nested(
mock.patch.object(os.path, 'realpath'),
mock.patch.object(libvirt_driver, '_run_multipath')
) as (_mock_os_path, mock_run_multipath):
with mock.patch.object(libvirt_driver, '_run_multipath') as run_multipath, \
mock.patch.object(os.path, 'realpath') as os_path:
4, side affect
import nova_cc_common @mock.patch.object(nova_cc_common.hookenv, 'config') def test_foo(mock_config): data = {'config-flags', 'foo=bar'} def fake_config(key): return data.get(key) mock_config.side_effect = fake_config
5, mock in global import
有时候, 有一个module级别的变量, 如hooks.nova_cc_common.py中有一个global module level的一行(conf = hookenv.config('config-flags') ), test_nova_cc_contexts.py中的这一行import hooks.nova_cc_context as context会再import hooks.nova_cc_common.py, 这样就出问题了. 所以需要mock global import:
with mock.patch('charmhelpers.core.hookenv.config'):
import hooks.nova_cc_context as context
Reference:
http://www.voidspace.org.uk/python/mock/mock.html