Openstack中的测试 ( by quqi99 )

 

                                                                           Openstack中的测试 ( by quqi99 )

 

作者:张华  发表于:2013-01-23
版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本版权声明

( http://blog.csdn.net/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
     

你可能感兴趣的:(OpenStack,Non-Networking)