Skip to content

管理节点基于模拟器的Integration Test框架

mingjian.deng edited this page Apr 27, 2017 · 25 revisions

目录

前言

作为产品型的IaaS项目,ZStack非常重视测试,我们要求每个功能、用户场景都有对应的测试用例覆盖。ZStack的测试有多种维度,本文介绍后端Java开发人员使用的基于模拟器的Integration Test框架。

ZStack的运行过程中,实际上是管理节点进程(Java编写)通过HTTP PRC调用控制部署在数据中心各物理设备上的Agent(Python或Golang编写),如下图:

在Integreation Test中,我们用模拟器(通过内嵌的Jetty Server)实现所有Agent HTTP RPC接口,每个用例的JVM进程就是一个自包含的ZStack环境,如图:

实例

先看一个实际例子

package org.zstack.test.integration.kvm.lifecycle

import org.springframework.http.HttpEntity
import org.zstack.header.vm.VmInstanceState
import org.zstack.header.vm.VmInstanceVO
import org.zstack.kvm.KVMAgentCommands
import org.zstack.kvm.KVMConstant
import org.zstack.sdk.VmInstanceInventory
import org.zstack.test.integration.kvm.OneVmBasicEnv
import org.zstack.testlib.EnvSpec
import org.zstack.testlib.SubCase
import org.zstack.testlib.VmSpec
import org.zstack.utils.gson.JSONObjectUtil

class OneVmBasicLifeCycleCase extends SubCase {
    EnvSpec env

    def DOC = """
test a VM's start/stop/reboot/destroy/recover operations 
"""

    @Override
    void setup() {
        spring {
            sftpBackupStorage()
            localStorage()
            virtualRouter()
            securityGroup()
            kvm()
        }
    }

    @Override
    void environment() {
        env = OneVmBasicEnv.env()
    }

    @Override
    void test() {
        env.create {
            testStopVm()
            testStartVm()
            testRebootVm()
            testDestroyVm()
            testRecoverVm()
        }
    }

    void testRecoverVm() {
        VmSpec spec = env.specByName("vm")

        VmInstanceInventory inv = recoverVmInstance {
            uuid = spec.inventory.uuid
        }

        assert inv.state == VmInstanceState.Stopped.toString()

        // confirm the vm can start after being recovered
        testStartVm()
    }

    void testDestroyVm() {
        VmSpec spec = env.specByName("vm")

        KVMAgentCommands.DestroyVmCmd cmd = null

        env.afterSimulator(KVMConstant.KVM_DESTROY_VM_PATH) { rsp, HttpEntity<String> e ->
            cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.DestroyVmCmd.class)
            return rsp
        }

        destroyVmInstance {
            uuid = spec.inventory.uuid
        }

        assert cmd != null
        assert cmd.uuid == spec.inventory.uuid
        VmInstanceVO vmvo = dbFindByUuid(cmd.uuid, VmInstanceVO.class)
        assert vmvo.state == VmInstanceState.Destroyed
    }

    void testRebootVm() {
        // reboot = stop + start
        VmSpec spec = env.specByName("vm")

        KVMAgentCommands.StartVmCmd startCmd = null
        KVMAgentCommands.StopVmCmd stopCmd = null

        env.afterSimulator(KVMConstant.KVM_STOP_VM_PATH) { rsp, HttpEntity<String> e ->
            stopCmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StopVmCmd.class)
            return rsp
        }

        env.afterSimulator(KVMConstant.KVM_START_VM_PATH) { rsp, HttpEntity<String> e ->
            startCmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StartVmCmd.class)
            return rsp
        }

        VmInstanceInventory inv = rebootVmInstance {
            uuid = spec.inventory.uuid
        }

        assert startCmd != null
        assert startCmd.vmInstanceUuid == spec.inventory.uuid
        assert stopCmd != null
        assert stopCmd.uuid == spec.inventory.uuid
        assert inv.state == VmInstanceState.Running.toString()
    }

    void testStartVm() {
        VmSpec spec = env.specByName("vm")

        KVMAgentCommands.StartVmCmd cmd = null

        env.afterSimulator(KVMConstant.KVM_START_VM_PATH) { rsp, HttpEntity<String> e ->
            cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StartVmCmd.class)
            return rsp
        }

        VmInstanceInventory inv = startVmInstance {
            uuid = spec.inventory.uuid
        }

        assert cmd != null
        assert cmd.vmInstanceUuid == spec.inventory.uuid
        assert inv.state == VmInstanceState.Running.toString()

        VmInstanceVO vmvo = dbFindByUuid(cmd.vmInstanceUuid, VmInstanceVO.class)
        assert vmvo.state == VmInstanceState.Running
        assert cmd.vmInternalId == vmvo.internalId
        assert cmd.vmName == vmvo.name
        assert cmd.memory == vmvo.memorySize
        assert cmd.cpuNum == vmvo.cpuNum
        //TODO: test socketNum, cpuOnSocket
        assert cmd.rootVolume.installPath == vmvo.rootVolumes.installPath
        assert cmd.useVirtio
        vmvo.vmNics.each { nic ->
            KVMAgentCommands.NicTO to = cmd.nics.find { nic.mac == it.mac }
            assert to != null: "unable to find the nic[mac:${nic.mac}]"
            assert to.deviceId == nic.deviceId
            assert to.useVirtio
            assert to.nicInternalName == nic.internalName
        }
    }

    void testStopVm() {
        VmSpec spec = env.specByName("vm")

        KVMAgentCommands.StopVmCmd cmd = null

        env.afterSimulator(KVMConstant.KVM_STOP_VM_PATH) { rsp, HttpEntity<String> e ->
            cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StopVmCmd.class)
            return rsp
        }

        VmInstanceInventory inv = stopVmInstance {
            uuid = spec.inventory.uuid
        }

        assert inv.state == VmInstanceState.Stopped.toString()

        assert cmd != null
        assert cmd.uuid == spec.inventory.uuid

        def vmvo = dbFindByUuid(cmd.uuid, VmInstanceVO.class)
        assert vmvo.state == VmInstanceState.Stopped
    }

    @Override
    void clean() {
        env.delete()
    }
}

ZStack的Integreation Test使用groovy编写,通过JUnit运行。运行如下命令可以执行该case:

cd /root/zstack/test
mvn test -Dtest=OneVmBasicLifeCycleCase

依赖环境

在运行任何Integration Test,开发者的开发环境需要满足如下条件:

  1. 从github上获得一份ZStack源代码

  2. 系统中安装了Mariadb数据库(或mysql)并运行,且数据库root用户的密码为空

    Integreation Test启动时会部署ZStack数据库,需要使用到数据库root用户,默认使用空密码,此项可以通过配置文件改变

  3. 系统中已安装了rabbitmq并运行,rabbitmq的guest用户使用默认密码

我们强烈建议开发者使用一个干净的CentOS作为开发环境,不要把Integration Test和运行ZStack的试验环境放在同一台机器,Integration Test运行时部署数据库的操作会导致试验环境的ZStack数据库丢失。

编写测试用例

构成

所有Integreation Test(除后文讲到的Test Suite)都继承SubCase类,例如:

class OneVmBasicLifeCycleCase extends SubCase {

并实现4个抽象函数:

  1. setup:配置用例,主要用于加载运行用例需要用到的ZStack服务和组件
  2. environment: 构造测试环境,例如创建zone、cluster,添加host等操作
  3. test:执行具体测试代码
  4. clean:清理环境 (仅当该case在test suite中运行时执行,后文详述)

测试用例运行时,上述4个函数依次执行,任何一个环节出现错误则测试终止退出(case在test suite中运行时例外)。

配置测试用例: setup()

ZStack采用Spring框架,通过XML文件配置和管理要加载的服务和组件。XML配置文件存在于两个目录:

  1. conf: 包含所有ZStack组件的XML配置文件

  2. test/src/test/resources/springConfigXml:包含测试用例自有的XML配置文件(例如有的测试用例可能增加自己组件或服务),以及用于覆盖默认组件的的XML配置文件(例如test/src/test/resources/springConfigXml/Kvm.xml)在测试时就会覆盖默认的conf/springConfigXml/Kvm.xml

    开发者可以使用覆盖默认组件的XML配置文件实现测试时改变默认组件的加载行为

setup()函数中,我们需要指定该测试用例需要加载哪些组件的XML配置文件,这通过spring函数的DSL语法实现,例如:

    @Override
    void setup() {
        spring {
            sftpBackupStorage()
            localStorage()
            virtualRouter()
            securityGroup()
            kvm()
        }
    }

这里使用的DSL是典型的groovy builder pattern,在整个Integreation Test框架中我们大量使用了该DSL pattern。

spring函数后的{} body中,开发者可以通过include()函数指定要加载的XML文件,例如:

    @Override
    void setup() {
        spring {
            include("Kvm.xml") //指定XML文件名即可,不需要加路径
            include("eip.xml")
        }
    }

或者使用spring DSL内置的函数直接加载相关XML文件,例如kvm()就等同于include("Kvm.xml")。目前spring DSL提供如下默认内置函数:

函数名 描述
includeAll 加载系统中所有组件
includeCoreServices 加载系统核心服务
nfsPrimaryStorage 加载NFS主存储服务
localStorage 加载本地主存储服务
vyos 加载vyos云路由服务
virtualRouter 加载虚拟路由服务
sftpBackupStorage 加载sftp备份存储服务
eip 加载eip服务
lb 加载负载均衡服务
portForwarding 加载端口转发服务
kvm 加载kvm服务
ceph 加载ceph主存储/备份存储服务
smp 加载sharedMountPoint主存储服务
securityGroup 加载安全组服务

其中includeAll不应该被直接使用,开发者应该只加载测试用例使用的组件和服务。includeCoreServices会被spring DSL默认调用,例如:

    @Override
    void setup() {
        spring {
            // 虽然没有指定任何XML,spring DSL仍然会为我们加载核心服务
        }
    }

会默认加载ZStack核心服务。SpringSpec.groovy包含了核心服务定义:

    List<String> CORE_SERVICES = [
            "HostManager.xml",
            "ZoneManager.xml",
            "ClusterManager.xml",
            "PrimaryStorageManager.xml",
            "BackupStorageManager.xml",
            "ImageManager.xml",
            "HostAllocatorManager.xml",
            "ConfigurationManager.xml",
            "VolumeManager.xml",
            "NetworkManager.xml",
            "VmInstanceManager.xml",
            "AccountManager.xml",
            "NetworkService.xml",
            "volumeSnapshot.xml",
            "tag.xml",
    ]

核心服务是指 提供一个基础IaaS环境所需要的服务,并非指启动ZStack进程所需要的服务。例如我们完全可以启动一个不包含VmInstanceManager.xml(虚拟机服务)的ZStack进程,它仍然工作,例如可以提供物理机相关的API,但不能提供虚拟机相关API(因为虚拟机服务没有加载)。

核心服务之间大多并不相互依赖,例如ZoneManager.xml并不依赖于HostManager.xml。当开发人员想细粒度的控制系统加载的服务,可以通过将INCLUDE_CORE_SERVICES变量设置成false以阻止spring DSL自动加载核心服务,例如:

    @Override
    void setup() {
        INCLUDE_CORE_SERVICES = false //当该变量设置成false后,后续的spring DSL不会自动加载核心服务

        spring {
            include("ZoneManager.xml") //这里我们只加载跟zone相关的服务
        }
    }

spring DSL提供的内置函数定义在SpringSpec.groovy中,开发人员可以直接查看。随着后续ZStack服务的增加,还会有新的内置函数加入。

构建用例环境:environment()

绝大部分Integreation Test需要构建测试环境,例如要测试停止虚拟机,首先需要一个已经创建好的虚拟机,而创建一个虚拟机又必须事先创建好物理机、主存储、镜像、网络等资源。为了将开发者从构建环境的重复劳动中解放出来,Integreation Test框架提供env DSL帮助自动创建环境,先看一个例子:

    EnvSpec myenv

    @Override
    void environment() {
        myenv = env {
            zone {
                name = "zone"

                l2NoVlanNetwork {
                    name = "l2"
                    physicalInterface = "eth0"

                    l3Network {
                        name = "l3"

                        ip {
                            name = "ipr"
                            startIp = "10.223.110.10"
                            endIp = "10.223.110.20"
                            gateway = "10.223.110.1"
                            netmask = "255.255.255.0"
                        }
                    }
                }
            }
        }
    }

在这个例子中,我们通过env DSL描述了一个环境,里面包含zone、l2NoVlanNetwork、l3Network、ip共4个资源。这里env()函数是env DSL的入口,用于创建一个EnvSpec对象,开发者可以直接调用其create()方法部署整个环境:

  @Override
  void test() {
     myenv.create()
  }

env DSL语法中每个资源可以包含三种成员:

  1. 参数:用于创建该资源的参数,例如name = "zone"就指定了创建该zone的name参数
  2. 子资源:例如l2NoVlanNetwork包含在zone中,它就是zone的一个子资源
  3. 函数:通常用于引用其它资源或关联其它资源

create()函数调用时,测试框架会遍历env DSL定义的资源树,并调用相应资源的SDK API进行创建,例如zone就会使用SDK中的CreateZoneAction进行创建。所以env DSL实质是为不同资源在SDK中的Create Action的参数赋值。例如zone资源包含namedescription两个参数就对应了CreateZoneAction的name和description参数。

当一个资源被包含在另一个资源的描述中时,被包含的资源称为子资源,例如上例中l2NoVlanNetwork是zone的子资源。create()方法在遍历资源树时,会先创建父资源,再创建子资源。

当一个资源的创建依赖于其它资源时,需要使用useXXX()函数通过被依赖资源的名称引用该资源。例如:

                virtualRouterOffering {
                    name = "vr"
                    memory = SizeUnit.MEGABYTE.toByte(512)
                    cpu = 2
                    useManagementL3Network("pubL3")
                    usePublicL3Network("pubL3")
                    useImage("vr")
                    useAccount("xin")
                }

对于virtualRouterOffering资源,其SDKCreateVirtualRouterOfferingAction需要指定managementNetworkUuidpublicNetworkUuidimageUuid字段,我们用useManagementL3NetworkusePublicL3NetworkuseImage去引用名为pubL3三层网络和名为vr的镜像,它们都是virtualRouterOffering的被依赖资源。create()函数遍历资源树时,会首先创建被依赖资源,例如这里会保证pubL3三层网络和vr镜像先于virtualRouterOffering之前创建,并且在创建virtualRouterOffering时自动为managementNetworkUuidpublicNetworkUuidimageUuid字段赋上相应资源的UUID值。

某些资源(例如cluster、zone)也可以使用函数去关联其它资源,例如cluster可以加载primary storage和l2network,则需要使用attachPrimaryStorage()attachL2Network()函数:

                cluster {
                    name = "cluster"
                    hypervisorType = "KVM"

                    kvm {
                        name = "kvm"
                        managementIp = "localhost"
                        username = "root"
                        password = "password"
                        usedMem = 1000
                        totalCpu = 10
                    }

                    attachPrimaryStorage("nfs", "ceph-pri", "local", "smp")
                    attachL2Network("l2")
                }

在上例中,cluster会加载"nfs", "ceph-pri", "local", "smp"等4个primary storage以及名为"l2"的l2network,create()函数在遍历资源树时会保证这些资源在attach操作时就已经创建完成。

env()函数返回的EnvSpec对象是integreation test核心对象,通常应该保存成为测试用例的一个成员变量,例如:

class OneL3OneIpRangeNoIpUsed extends SubCase {
    EnvSpec env
    
    @Override
    void environment() {
        env = env {
            //在这里描述环境
        }
    }
    
    @Override
    void test() {
        env.create {
            //这里执行测试逻辑
        }
    }
}

EnvSpec.create()可以接受一个函数作为参数,具体测试的函数都包含在该函数中运行。

env DSL清单

env DSL目前支持的所有资源、参数、函数如下:

└── env
    ├── account
    │   ├── (field required) name
    │   └── (field required) password
    ├── cephBackupStorage
    │   ├── (field optional) availableCapacity
    │   ├── (field optional) description
    │   ├── (field optional) monAddrs
    │   ├── (field optional) totalCapacity
    │   ├── (field required) fsid
    │   ├── (field required) monUrls
    │   ├── (field required) name
    │   └── (field required) url
    ├── diskOffering
    │   ├── (field optional) allocatorStrategy
    │   ├── (field optional) description
    │   ├── (field required) diskSize
    │   ├── (field required) name
    │   └── (method) useAccount
    ├── instanceOffering
    │   ├── (field optional) allocatorStrategy
    │   ├── (field optional) cpu
    │   ├── (field optional) description
    │   ├── (field optional) memory
    │   ├── (field required) name
    │   └── (method) useAccount
    ├── sftpBackupStorage
    │   ├── (field optional) availableCapacity
    │   ├── (field optional) description
    │   ├── (field optional) hostname
    │   ├── (field optional) password
    │   ├── (field optional) totalCapacity
    │   ├── (field optional) username
    │   ├── (field required) name
    │   └── (field required) url
    ├── vm
    │   ├── (field optional) description
    │   ├── (field required) name
    │   ├── (method) useAccount
    │   ├── (method) useCluster
    │   ├── (method) useDefaultL3Network
    │   ├── (method) useDiskOfferings
    │   ├── (method) useHost
    │   ├── (method) useImage
    │   ├── (method) useInstanceOffering
    │   ├── (method) useL3Networks
    │   └── (method) useRootDiskOffering
    └── zone
        ├── (field optional) description
        ├── (field required) name
        ├── (method) attachBackupStorage
        ├── cephPrimaryStorage
        │   ├── (field optional) availableCapacity
        │   ├── (field optional) description
        │   ├── (field optional) monAddrs
        │   ├── (field optional) totalCapacity
        │   ├── (field required) fsid
        │   ├── (field required) monUrls
        │   ├── (field required) name
        │   └── (field required) url
        ├── cluster
        │   ├── (field optional) description
        │   ├── (field required) hypervisorType
        │   ├── (field required) name
        │   ├── (method) attachL2Network
        │   ├── (method) attachPrimaryStorage
        │   └── kvm
        │       ├── (field optional) description
        │       ├── (field optional) managementIp
        │       ├── (field optional) totalCpu
        │       ├── (field optional) totalMem
        │       ├── (field optional) usedCpu
        │       ├── (field optional) usedMem
        │       ├── (field required) name
        │       ├── (field required) password
        │       └── (field required) username
        ├── eip
        │   ├── (field optional) description
        │   ├── (field optional) requiredIp
        │   ├── (field required) name
        │   ├── (method) useAccount
        │   ├── (method) useVip
        │   └── (method) useVmNic
        ├── l2NoVlanNetwork
        │   ├── (field optional) description
        │   ├── (field required) name
        │   └── (field required) physicalInterface
        ├── l2VlanNetwork
        │   ├── (field optional) description
        │   ├── (field required) name
        │   ├── (field required) physicalInterface
        │   └── (field required) vlan
        ├── lb
        │   ├── (field optional) description
        │   ├── (field required) name
        │   ├── (method) useAccount
        │   ├── (method) useVip
        │   └── listener
        │       ├── (field optional) description
        │       ├── (field required) instancePort
        │       ├── (field required) loadBalancerPort
        │       ├── (field required) name
        │       ├── (field required) protocol
        │       └── (method) useAccount
        ├── localPrimaryStorage
        │   ├── (field optional) availableCapacity
        │   ├── (field optional) description
        │   ├── (field optional) totalCapacity
        │   ├── (field required) name
        │   └── (field required) url
        ├── nfsPrimaryStorage
        │   ├── (field optional) availableCapacity
        │   ├── (field optional) description
        │   ├── (field optional) totalCapacity
        │   ├── (field required) name
        │   └── (field required) url
        ├── portForwarding
        │   ├── (field optional) allowedCidr
        │   ├── (field optional) description
        │   ├── (field required) name
        │   ├── (field required) privatePortEnd
        │   ├── (field required) privatePortStart
        │   ├── (field required) protocolType
        │   ├── (field required) vipPortEnd
        │   ├── (field required) vipPortStart
        │   ├── (method) useAccount
        │   ├── (method) useVip
        │   └── (method) useVmNic
        ├── securityGroup
        │   ├── (field optional) description
        │   ├── (field required) name
        │   ├── (method) attachL3Network
        │   ├── (method) useAccount
        │   ├── (method) useVmNic
        │   └── rule
        │       ├── (field optional) allowedCidr
        │       ├── (field required) endPort
        │       ├── (field required) protocol
        │       ├── (field required) startPort
        │       └── (field required) type
        ├── smpPrimaryStorage
        │   ├── (field optional) availableCapacity
        │   ├── (field optional) description
        │   ├── (field optional) totalCapacity
        │   ├── (field required) name
        │   └── (field required) url
        └── virtualRouterOffering
            ├── (field optional) allocatorStrategy
            ├── (field optional) cpu
            ├── (field optional) description
            ├── (field optional) isDefault
            ├── (field optional) memory
            ├── (field required) name
            ├── (method) useAccount
            ├── (method) useImage
            ├── (method) useManagementL3Network
            └── (method) usePublicL3Network


执行测试逻辑:test()

具体的测试逻辑包含在test()函数中,作为integreation test,开发人员应该更多从API层面验证程序功能。

用函数名作为注释

一个integreation test通常包含多个程序逻辑的验证,相互混杂在一起常常让阅读代码的人不能直观的了解测试逻辑。ZStack要求每个独立的测试逻辑都封装到一个函数中,并使用函数名作为测试逻辑的注释。例如:

    void useIpRangeUuidWithStartBeyondTheEndIp() {
        IpRangeSpec ipr = env.specByName("ipr")

        List<FreeIpInventory> freeIps = getFreeIpOfIpRange {
            ipRangeUuid = ipr.inventory.uuid
            start = "10.223.110.21"
        }

        assert freeIps.size() == 0
    }

该函数包含在org.zstack.test.integration.l3network.getfreeip.OneL3OneIpRangeNoIpUsed类中,通过类名和函数名,我们能够很容易的理解这个函数测试的逻辑是:测试getfreeip API,并且使用了一个l3network和一个iprange,目前iprange中没有ip被占用;通过iprange uuid去获取freeip指定API的start参数,而且该参数已经超过了iprange的end ip。

命名规则如下:

  1. 通过package名描述测试资源的场景,例如getfreeip是l3network的一个场景,而org.zstack.test.integration.kvm.lifecycle是kvm的lifecycle场景。每个新场景都需要创建一个新的子package。
  2. 通过class名描述部署环境,例如OneL3OneIpRangeNoIpUsedOneVmBasicLifeCycleCase都能表示大概的部署场景。
  3. 通过函数名描述测试的具体内容,例如useIpRangeUuidWithStartBeyondTheEndIptestStopVm
  4. 如果名字太长,英语中的一些介词可以省略,例如useIpRangeUuidWithStartBeyondTheEndIp可以省掉withthe变成useIpRangeUuidStartBeyondEndIp

每个测试函数只包含一个测试场景

测试场景应该进行细粒度分割,保证每个函数中只有一个测试场景,方便阅读,例如下面这个例子只测试停止VM一个场景:

    void testStopVm() {
        VmSpec spec = env.specByName("vm")

        KVMAgentCommands.StopVmCmd cmd = null

        env.afterSimulator(KVMConstant.KVM_STOP_VM_PATH) { rsp, HttpEntity<String> e ->
            cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StopVmCmd.class)
            return rsp
        }

        VmInstanceInventory inv = stopVmInstance {
            uuid = spec.inventory.uuid
        }

        assert inv.state == VmInstanceState.Stopped.toString()

        assert cmd != null
        assert cmd.uuid == spec.inventory.uuid

        def vmvo = dbFindByUuid(cmd.uuid, VmInstanceVO.class)
        assert vmvo.state == VmInstanceState.Stopped
    }

我们允许调用一个测试场景的函数来验证另一个测试场景。例如测试recover VM这个功能时,我们要确认被recover的VM可以成功启动,则可以在测试recover VM的函数中调用测试start VM的函数进行验证:

    void testRecoverVm() {
        VmSpec spec = env.specByName("vm")

        VmInstanceInventory inv = recoverVmInstance {
            uuid = spec.inventory.uuid
        }

        assert inv.state == VmInstanceState.Stopped.toString()

        // confirm the vm can start after being recovered
        testStartVm()
    }

调用API

Integreation Test中大部分时候是基于API对具体场景进行测试。所有测试用例必须使用ZStack Java SDK调用API,任何其他形式都是禁止的(例如通过CloudBus发API Message)。为了方便API调用,Integreation Test将所有Java SDK封装成了API DSL。例如使用SDK启动一个云主机,写法为:

StartVmInstanceAction a = new StartVmInstanceAction()
a.sessionId = "583c56b6352d4399aac23295b1507506"
a.uuid = "36c27e8ff05c4780bf6d2fa65700f22e"
StartVmInstanceAction.Result res = a.call()
assert res.error != null: "API StartVmInstanceAction fails with an error ${res.error}"
VmInstanceInventory vm = res.value.inventory

使用API DSL代码则简化为:

VmInstanceInventory inv = startVmInstance {
     uuid = "36c27e8ff05c4780bf6d2fa65700f22e"
     sessionId = "583c56b6352d4399aac23295b1507506"
}

API DSL会自动检查返回值,如果error不为空则assert异常。

如果一个API失败的行为是期望的,可以用expect函数。expect的第一个参数可以是一个Throwable Class,也可以是一个Throwable Class的集合:

    expect(RuntimeException.class) {
        throw new RuntimeException("ok")
    }

    expect([CloudRuntimeException.class, IllegalArgumentException.class]) {
        throw new RuntimeException("ok")
    }

    expect(AssertionError.class) {
        VmInstanceInventory inv = startVmInstance {
            uuid = "36c27e8ff05c4780bf6d2fa65700f22e"
            sessionId = "583c56b6352d4399aac23295b1507506"
        }
    }

如果expect后的函数抛出的异常不是所期望的,expect本身则会抛出一个Exception导致测试失败。

API DSL的函数命名方式很简单,将SDK对应类名的Action去掉,并且首字母小写就是对应的函数名。例如StartVmInstanceAction对应startVmInstance。使用Intellij等IDE输入函数名时又自动提示和补全。

由于API DSL会自动检查返回值,如果返回error是预期行为并想对error进行检查,则不能使用API DSL,而要使用SDK。

通过assert来验证测试结果

测试用例在验证测试结果的时候可以使用groovy的assert功能来验证结果,例如:

assert inv.state == VmInstanceState.Stopped.toString()

当验证失败时,log里面也会有详细信息:

assert freeIps.size() == 10
       |       |      |
       []      0      false org.codehaus.groovy.runtime.powerassert.PowerAssertionError: assert freeIps.size() == 10
       |       |      |
       []      0      false
	... suppressed 2 lines
	at org.zstack.test.integration.l3network.getfreeip.OneL3OneIpRangeNoIpUsed.useIpRangeUuidWithStartBeyondTheEndIp(OneL3OneIpRangeNoIpUsed.groovy:76) ~[test-classes/:?]
	... suppressed 12 lines
	at org.zstack.test.integration.l3network.getfreeip.OneL3OneIpRangeNoIpUsed$_test_closure3.doCall(OneL3OneIpRangeNoIpUsed.groovy:60) ~[test-classes/:?]
	at org.zstack.test.integration.l3network.getfreeip.OneL3OneIpRangeNoIpUsed$_test_closure3.doCall(OneL3OneIpRangeNoIpUsed.groovy) ~[test-classes/:?]
	... suppressed 12 lines
	at org.zstack.testlib.EnvSpec.create(EnvSpec.groovy:229) ~[testlib-1.9.0.jar:?]
	at org.zstack.testlib.EnvSpec$create.call(Unknown Source) ~[?:?]

模拟agent行为

ZStack Integreation Test最核心功能是通过基于Jetty的模拟器模拟真实环境下物理设备上安装的agent,例如模拟物理机上安装的KVM agent。当测试的场景涉及到后端agent调用时,我们需要捕获这些HTTP请求并进行验证,也可以伪造agent返回测试API逻辑。

EnvSpec提供simulator()afterSimulator()模拟agent行为,两者的区别在于simulator()会替换测试框架默认的处理函数,而afterSimulator()允许在默认处理函数执行完后再执行一段额外的逻辑。例如

env.simulator(KVMConstant.KVM_START_VM_PATH) {
    throw new Exception("fail to start a VM on purpose")
}

在上例中,我们通过simulator()替换掉了框架对KVMConstant.KVM_START_VM_PATH的默认处理函数,并在我们自己的处理函数中抛出了一个异常来模拟启动VM失败的情况。而使用afterSimulator()则可以在默认处理函数执行完后增加一段逻辑,例如下面例子中,我们捕获了发往KVMConstant.KVM_START_VM_PATH的命令,并对相关字段进行了验证:

void testStartVm() {
    VmSpec spec = env.specByName("vm")

    KVMAgentCommands.StartVmCmd cmd = null

    env.afterSimulator(KVMConstant.KVM_START_VM_PATH) { rsp, HttpEntity<String> e ->
        cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StartVmCmd.class)
        return rsp
    }

    VmInstanceInventory inv = startVmInstance {
        uuid = spec.inventory.uuid
    }

    assert cmd != null
    assert cmd.vmInstanceUuid == spec.inventory.uuid
    assert inv.state == VmInstanceState.Running.toString()

    VmInstanceVO vmvo = dbFindByUuid(cmd.vmInstanceUuid, VmInstanceVO.class)
    assert vmvo.state == VmInstanceState.Running
    assert cmd.vmInternalId == vmvo.internalId
    assert cmd.vmName == vmvo.name
    assert cmd.memory == vmvo.memorySize
    assert cmd.cpuNum == vmvo.cpuNum
    //TODO: test socketNum, cpuOnSocket
    assert cmd.rootVolume.installPath == vmvo.rootVolumes.installPath
    assert cmd.useVirtio
    vmvo.vmNics.each { nic ->
        KVMAgentCommands.NicTO to = cmd.nics.find { nic.mac == it.mac }
        assert to != null: "unable to find the nic[mac:${nic.mac}]"
        assert to.deviceId == nic.deviceId
        assert to.useVirtio
        assert to.nicInternalName == nic.internalName
    }
}

测试框架对所有HTTP RPC都注册了返回执行成功的默认handler。simulator()afterSimulator()仅仅改变所关联EnvSpec对象上的agent逻辑,不影响其它EnvSpec对象。

simulator()定义

当我们希望改变测试框架默认handler的行为,使用simulator()

// httpPath: agent的HTTP RPC路径,例如上例中的KVM_START_VM_PATH = "/vm/start"
// handler: 处理HTTP RPC调用的函数
void simulator(String httpPath, Closure handler)

// handler作为groovy Closure类型可以接收两个可选参数:
// entity:HTTP request,可以获得HTTP header和body
// spec: 该handler挂载的EnvSpec,可以通过它获得其它资源的spec
// 返回值:返回给HTTP PRC调用的response,如果该HTTP RPC不需要返回值,则返回一个空map:[:]或null。
def handler = { HttpEntity<String> entity, EnvSpec spec ->
    return [:]
}
afterSimulator()定义

当我们不希望改变测试框架默认handler的行为,仅仅希望捕获HTTP RPC命令,或者改变返回的response时,用afterSimulator()

// httpPath: agent的HTTP RPC路径,例如上例中的KVM_START_VM_PATH = "/vm/start"
// handler: 需要在系统默认handler执行后被调用的函数
void afterSimulator(String httpPath, Closure handler)

// handler可以接收三个可选参数
// response: 系统默认handler返回的response对象
// entity:HTTP request,可以获得HTTP header和body
// spec: 该handler挂载的EnvSpec,可以通过它获得其它资源的spec
// 返回值:返回给HTTP PRC调用的response,如果该HTTP RPC不需要返回值,则返回一个空map:[:]或null。
def handler = { Object response, HttpEntity<String> entity, EnvSpec spec ->
    return response
}

模拟HTTP错误

我们可以在simulator()afterSimulator()函数中抛出HttpError异常模拟HTTP错误,例如:

env.simulator(KVMConstant.KVM_START_VM_PATH) {
    throw new HttpError(403, "fail to start a VM on purpose")
}

捕获消息

我们可以用EnvSpec.message()捕获一个消息,并模拟消息的行为,例如:

@Override
void test() {
    ErrorFacade errf = bean(ErrorFacade.class)

    env.message(StartNewCreatedVmInstanceMsg.class) { StartNewCreatedVmInstanceMsg msg, CloudBus bus ->
        def reply = new MessageReply()
        reply.setError(errf.stringToOperationError("on purpose"))
        bus.reply(msg, reply)
    }
}

这里我们捕获了StartNewCreatedVmInstanceMsg消息并制造了一个错误作为消息返回。message()还可以接受一个条件函数用来选择性捕获某些消息,例如:

@Override
void test() {
    ErrorFacade errf = bean(ErrorFacade.class)

    message(StartNewCreatedVmInstanceMsg.class, { StartNewCreatedVmInstanceMsg msg ->
        return msg.vmInstanceInventory.name == "web"
    }) { StartNewCreatedVmInstanceMsg msg, CloudBus bus ->
        def reply = new MessageReply()
        reply.setError(errf.stringToOperationError("on purpose"))
        bus.reply(msg, reply)
    }
}

在这里例子中,只有当msg.vmInstanceInventory.name == "web"这个条件满足时,消息才会被捕获。

message() 定义
// msgClz: 要捕获消息的类型
// condition: 条件函数,当函数返回true时,消息才会被捕获并执行handler
// handler: 处理被捕获消息的函数
void message(Class<? extends Message> msgClz, Closure condition, Closure handler)

// condition接收一个可选参数
// msg: 被捕获的消息
// 返回值: true捕获消息,false不捕获消息
def condition = { Message msg ->
    return true
}

// handler接收两个可选参数
// msg: 被捕获的消息
// bus: CloudBus对象
// 无返回值
def handler = { Message msg, CloudBus bus ->
}

执行多个测试场景

用例通常包含多个测试场景,执行时应该按顺序包含在EnvSpec.create()函数接收的Closure中,例如:

@Override
void test() {
    env.create {
        testStopVm()
        testStartVm()
        testRebootVm()
        testDestroyVm()
        testRecoverVm()
    }
}

销毁测试环境:clean()

每个测试用例都应该在clean()函数中销毁在environment()中构建的EnvSpec对象,例如:

@Override
void clean() {
    env.delete()
}

测试用例单独执行时clean()不会被调用,以留存数据库环境供手动分析

Test Suite

测试用例可以单独执行,也可以放在test suite一起执行。test suite的作用是只启动一次JVM和ZStack环境就运行所有测试用例,大大减少测试时间。例如:

class GetFreeIpTest extends Test {
    def DOC = """

    Test getting free IPs from a single L3 network with one or two IP ranges
    
"""
    @Override
    void setup() {
        spring {
            include("vip.xml")
        }
    }

    @Override
    void environment() {
    }

    @Override
    void test() {
        runSubCases()
    }
}

test suite的整体结构跟测试用例类似,不同点在于:

  1. test suite继承Test类,测试用例继承SubCase
  2. test suite的environment()通常为空,因为各测试用例会自己创建的EnvSpec对象
  3. test suite在test()函数中通过runSubCases()执行一组测试用例
  4. test suite没有clean()函数

测试用例必须保证跟test suite加载相同的服务和组件,以保证用例单独执行和在test suite中执行时ZStack运行的组件和服务完全相同。故测试用例应该跟test suite有相同 setup()函数。

runSubCases()运行时会自动搜索该test suite所在package以及子package的所有测试用例,无需程序员显示指定。

运行Test Suite

运行test suite方法跟运行单个测试用例一样:

mvn test -Dtest=GetFreeIpTest

指定Test Suite输出结果目录

可以使用-DresultDir参数指定test Suite输出结果目录,例如:

mvn test -Dtest=GetFreeIpTest -DresultDir=/tmp

运行结束后,测试框架会在指定目录建立一个名为zstack-integration-test-result的子目录。每个test suite又有一个以class全名命名的子目录,例如org_zstack_test_integration_kvm_KvmTest,其中包含一个summary文件,包含该test suite运行的总体信息,例如:

{"total":1,"success":0,"failure":1,"passRate":0.0}

以及以每个测试用例class全名命名的结果文件,例如org_zstack_test_integration_kvm_lifecycle_OneVmBasicLifeCycleCase.failure:

{"success":false,"error":"unable to find the nic[ip:193.168.100.55]. Expression: (to !\u003d null). Values: to \u003d null","name":"OneVmBasicLifeCycleCase"}

文件的后缀名表示测试结果:success为成功,failure为失败。

所有文件的内容均为JSON格式

测试用例间共享env DSL

相同test suite中的测试用例常常需要共享相同的env DSL,则可以通过一个类的static函数共享,例如:

class OneVmBasicEnv {
    def DOC = """
use:
1. sftp backup storage
2. local primary storage
3. virtual router provider
4. l2 novlan network
5. security group
"""

    static EnvSpec env() {
        return Test.makeEnv {
            instanceOffering {
                name = "instanceOffering"
                memory = SizeUnit.GIGABYTE.toByte(8)
                cpu = 4
            }

            sftpBackupStorage {
                name = "sftp"
                url = "/sftp"
                username = "root"
                password = "password"
                hostname = "localhost"

                image {
                    name = "image1"
                    url  = "http://zstack.org/download/test.qcow2"
                }

                image {
                    name = "vr"
                    url  = "http://zstack.org/download/vr.qcow2"
                }
            }

            zone {
                name = "zone"
                description = "test"

                cluster {
                    name = "cluster"
                    hypervisorType = "KVM"

                    kvm {
                        name = "kvm"
                        managementIp = "localhost"
                        username = "root"
                        password = "password"
                    }

                    attachPrimaryStorage("local")
                    attachL2Network("l2")
                }

                localPrimaryStorage {
                    name = "local"
                    url = "/local_ps"
                }

                l2NoVlanNetwork {
                    name = "l2"
                    physicalInterface = "eth0"

                    l3Network {
                        name = "l3"

                        service {
                            provider = VirtualRouterConstant.PROVIDER_TYPE
                            types = [NetworkServiceType.DHCP.toString(), NetworkServiceType.DNS.toString()]
                        }

                        service {
                            provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE
                            types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE]
                        }

                        ip {
                            startIp = "192.168.100.10"
                            endIp = "192.168.100.100"
                            netmask = "255.255.255.0"
                            gateway = "192.168.100.1"
                        }
                    }

                    l3Network {
                        name = "pubL3"

                        ip {
                            startIp = "12.16.10.10"
                            endIp = "12.16.10.100"
                            netmask = "255.255.255.0"
                            gateway = "12.16.10.1"
                        }
                    }
                }

                virtualRouterOffering {
                    name = "vr"
                    memory = SizeUnit.MEGABYTE.toByte(512)
                    cpu = 2
                    useManagementL3Network("pubL3")
                    usePublicL3Network("pubL3")
                    useImage("vr")
                }

                attachBackupStorage("sftp")
            }

            vm {
                name = "vm"
                useInstanceOffering("instanceOffering")
                useImage("image1")
                useL3Networks("l3")
            }
        }
    }
}

class OneVmBasicLifeCycleCase extends SubCase {
    EnvSpec env

    def DOC = """
test a VM's start/stop/reboot/destroy/recover operations 
""" 

    @Override
    void environment() {
        env = OneVmBasicEnv.env()
    }

上例中OneVmBasicEnv类中包含了一个公共的env DSL,OneVmBasicLifeCycleCase用例在environment()函数中通过OneVmBasicEnv.env()构建了一个EnvSpec对象。

Cookbooks

如何在env DSL定义资源的时候指定其UUID

可以通过resourceUuid参数为env DSL定义的资源指定UUID,例如:

env = env {
    zone {
        resourceUuid = "14d087f6d59a4d639094e6c2c9032161"
        name = "zone1"
    }
}

如何进行级联创建

在某些情况下,我们需要进行资源的级联创建,尤其在VCenter和混合云中,我们有大量的sync操作(同步外部资源)。例如:
在添加一个远程的DataCenter时,我们需要把该DataCenter下的所有VPC全部同步过来,而在同步每个VPC时,又需要把该VPC下的所有VRouter同步过来,同样的,在同步VRouter时,我们需要同步该VRouter下的所有RouterInterface以及RouteEntry等。
此时,我们要测试程序自动进行级联创建,但又需要传入VPC、VRouter、RouterInterface以及RouteEntry的相关参数以便模拟。因此我们不能写成以下形式(简化起见,我们只关注VPC和VRouter):

            DataCenterSpec dcSpec = dataCenter {
                regionId = "cn-hangzhou"
                type = "aliyun"
                description = "createEcsEnv test"
                dcName = "Test-Region-Name"
                vpc = ecsVpc {
                    vpcName = "Test-Vpc-Name"
                    description = "Test-Vpc"
                    cidrBlock = "192.168.0.0/16"
                    vpcId = "Test-Vpc-Id"
                    VRouterId = "Test-VRouter-Id"
                    vrouter = vRouter {
                        vrId = "Test-VRouter-Id"
                        vRouterName = "Test-VRouter-Name"
                        description = "Test-VRouter"
                    }
                }
                postCreate {
                    attachToOssBucket(ossSpeck.inventory.uuid)
                }
            }

写成以上形式会报错,其一是因为测试程序在创建VRouter时,缺少vpcUuid。在创建VPC时,又缺少dataCenterUuid。其二是因为vRouter和VPC的创建应该由业务逻辑自行完成,而不是用户手工创建
为解决级联创建的问题,我们引入参数"onlyDefine",默认值为false。当需要级联创建时,我们只需把以上代码修改为

            DataCenterSpec dcSpec = dataCenter {
                regionId = "cn-hangzhou"
                type = "aliyun"
                description = "createEcsEnv test"
                dcName = "Test-Region-Name"
                vpc = ecsVpc {
                    onlyDefine = true   // 只需在这里设置true即可
                    vpcName = "Test-Vpc-Name"
                    description = "Test-Vpc"
                    cidrBlock = "192.168.0.0/16"
                    vpcId = "Test-Vpc-Id"
                    VRouterId = "Test-VRouter-Id"
                    vrouter = vRouter {
                        onlyDefine = true    // 只需在这里设置true即可
                        vrId = "Test-VRouter-Id"
                        vRouterName = "Test-VRouter-Name"
                        description = "Test-VRouter"
                    }
                }
                postCreate {
                    attachToOssBucket(ossSpeck.inventory.uuid)
                }
            }

然后,在相应的Spec文件中,我们定义一个define函数,如:

(in EcsVpcSpec.groovy)
    @Override
    SpecID define(String uuid) {
        inventory = new EcsVpcInventory()
        inventory.uuid = uuid
        inventory.vpcName = vpcName
        inventory.ecsVpcId = vpcId
        inventory.cidrBlock = cidrBlock
        inventory.description = description
        inventory.vRouterId = VRouterId
        inventory.status = "Available"

        return id(inventory.vpcName, inventory.uuid)
    }

以及

(in VRouterSpec.groovy)
    @Override
    SpecID define(String uuid) {
        inventory = new VpcVirtualRouterInventory()
        inventory.uuid = uuid
        inventory.vRouterName = vRouterName
        inventory.description = description
        inventory.vrId = vrId
        return id(inventory.vRouterName, inventory.uuid)
    }

如此以来,测试程序就创建出了相应的inventory,以便simulator使用,而不会去尝试写数据库。(写数据库操作应该由业务逻辑自行完成)
测试程序在创建dataCenter的时候,若要同步VPC,那么会发出一个SyncVpcPropertyMsg,测试程序捕捉到后,可以对其进行如下模拟,此时由于inventory己经被define了,所以该simulator可以通过

(in VRouterSpec.groovy)
    private void setupSimulator() {
        message(SyncVpcPropertyMsg.class) { SyncVpcPropertyMsg msg, CloudBus bus ->
            SyncVpcPropertyReply reply = new SyncVpcPropertyReply()
            def property = new EcsVpcProperty()
            property.ecsVpcId = inventory.ecsVpcId
            property.status = inventory.status
            property.vpcName = inventory.vpcName
            property.cidrBlock = inventory.cidrBlock
            property.vRouterId = inventory.vRouterId
            property.description = inventory.description
            reply.setVpcs(Arrays.asList(property))
            bus.reply(msg, reply)
        }
    }

如何在获得env DSL中定义的资源的spec

env DSL中定义的资源可以通过名字和UUID两种方式引用。例如:

    @Override
    void test() {
        // envSpec 为env DSL创建的EnvSpec对象
        envSpec.create {
            DiskOfferingSpec diskOfferingSpec = envSpec.specByName("diskOffering")
            ZoneSpec zone = envSpec.specsByUuid("14d087f6d59a4d639094e6c2c9032161")
        }
    }

env DSL描述资源时应该为每个资源赋予一个全局唯一的名字,以保证通过specByName()能引用到正确的资源。使用specsByUuid()引用资源时应保证该资源在env DSL中使用了resourceUuid参数指定UUID。

每个资源的spec对象都包含一个inventory字段,对应该资源在SDK中的inventory类,例如ZoneSpec.inventory类型为org.zstack.sdk.ZoneInventory

注意:SDK中的inventory类命名跟ZStack header package中的inventory类命名一样,因为SDK是通过ZStack源码生成的。在写测试用时,应注意不要错误的import了header package中的inventory类而引发类型错误。测试用例应该只使用SDK中的inventory类。

如何获取已加载的组件

可以通过bean()函数获得加载的ZStack组件,例如:

@Override
void test() {
    ErrorFacade errf = bean(ErrorFacade.class)
    DatabaseFacade dbf = bean(DatabaseFacade.class)
}

DatabaseFacade.findByUuid() 快捷函数

可以通过dbFindByUuid()函数方便的通过UUID查询一个资源的数据库VO对象,例如:

void testStartVm() {
    VmInstanceVO vmvo = dbFindByUuid(cmd.vmInstanceUuid, VmInstanceVO.class)
    assert vmvo.state == VmInstanceState.Running
}

相当于:

void testStartVm() {
    DatabaseFacade dbf = bean(DatabaseFacade.class)
    VmInstanceVO vmvo = dbf.findByUuid(cmd.vmInstanceUuid, VmInstanceVO.class)
    assert vmvo.state == VmInstanceState.Running
}

如何清理加载的simulator/message的handler

可以直接调用下列函数清除前面测试函数加载的simulator或message加载的handler:

// env为EnvSpec对象

env.cleanSimulatorAndMessageHandlers()
env.cleanSimulatorHandlers()
env.cleanAfterSimulatorHandlers()
env.cleanMessageHandlers()

JSON快捷函数

可以直接使用json()函数将json字符串转换成对象:

        env.afterSimulator(FlatUserdataBackend.RELEASE_USER_DATA) { rsp, HttpEntity<String> e ->
            cmd = json(e.body, FlatUserdataBackend.ReleaseUserdataCmd.class)
            return rsp
        }

应该在哪里修改Global Config

当一个case需要修改global config时,只能在EnvSpec.create()函数后的{}中,因为当create函数执行时会重置所有global config到默认值。例如:

  @Override
    void test() {
        env.create {
            // Global Config必须在这里修改
            // make the interval very long, we use api to trigger the job to test
            ImageGlobalConfig.DELETION_GARBAGE_COLLECTION_INTERVAL.updateValue(TimeUnit.DAYS.toSeconds(1))

            testImageGCWhenBackupStorageDisconnect()

            env.recreate("image")

            testImageGCCancelledAfterBackupStorageDeleted()
        }
    }

如何重建一个被删除的资源,该资源是用environment()构造的

有时候我们测试用例会删除一些资源做测试,而这些资源又是environment()构造的包含在EnvSpec对象中的资源。当用例中后面的测试函数需要用到这些资源时,重建是件非常麻烦的事情,这时可以用EnvSpec.recreate()函数重建该资源,例如:

    @Override
    void test() {
        env.create {
            testGCSuccess()
            testGCCancelledAfterHostDeleted()

            //这里 testGCCancelledAfterHostDeleted() 删除了名为kvm的host,我们
            //用env.recreate()重建它供testGCCancelledAfterPrimaryStorageDeleted()使用
            env.recreate("kvm")

            testGCCancelledAfterPrimaryStorageDeleted()
        }
    }

EnvSpec.recreate()会重建资源以及它的子资源。

获得一个资源的inventory对象

可以直接通过EnvSpec.inventoryByName()获得一个已创建资源的inventory对象(org.zstack.sdk.xxxInventory, 例如org.zstack.sdk.ImageInventory)。举例:

/*
EnvSpec env = env {
        zone {
            name = "zone"
        }

        sftpBackupStorage {
            name = "sftp"
            url = "/sftp"

            image {
                name = "image"
                url = "http://zstack.org/download/image.qcow2"
            }
        }
}
*/

ImageInventory image = env.inventoryByName("image")

使用retryInSecsretryInMillis检验异步操作结果

当某些操作异步执行时(例如删除虚拟机后,归还磁盘容量的就是异步操作),我们需要等待一段时间确保异步操作完成再检验结果,可以使用retryInXxx函数不断检测异步操作是否完成,具体使用方式见下例:

    boolean ret = retryInSecs(3, 1) {
        // 在这里执行操作结果检测
        // 检测成功返回true,则retryInSecs会直接返回true,表示检测成功;
        // 返回false,retryInSecs会sleep指定interval后(第二个参数,这里为1s)后再次执行该检测函数。
        // 如果在指定间隔时间(第一个参数,这里为3s)检测函数都返回false,retryInSecs返回false,表示检测失败
        return true
    }

同样可以用retryInMillis()进行毫秒级的循环检测。

查看失败case log

Test Suite运行时会将失败case的log以及当时的DB dump保存到zstack-integration-test-result/TEST-SUITE-DIR/failureLogs/CASE-NAME目录,例如

[root@localhost:/root/zstack/test]# ls zstack-integration-test-result/org_zstack_test_integration_network_NetworkTest/failureLogs/org_zstack_test_integration_network_vxlanNetwork_OneVxlanNetworkLifeCycleCase/
case.log  dbdump.sql

获取Test Suite测试用例列表

运行test suite时指定-Dlist参数可以获取测试用例列表,例如:

mvn test -Dtest=KvmTest -Dlist

列表输出在对应test suite结果目录的cases文件中,例如:

[root@localhost:/root/zstack/test]# cat zstack-integration-test-result/org_zstack_test_integration_kvm_KvmTest/cases 
org.zstack.test.integration.kvm.host.HostStateCase
org.zstack.test.integration.kvm.status.MaintainHostCase
org.zstack.test.integration.kvm.vm.VmConsoleCase
org.zstack.test.integration.kvm.hostallocator.LeastVmPreferredAllocatorCase
org.zstack.test.integration.kvm.vm.VmGCCase
org.zstack.test.integration.kvm.vm.OneVmBasicLifeCycleCase
org.zstack.test.integration.kvm.globalconfig.KvmGlobalConfigCase
org.zstack.test.integration.kvm.vm.UpdateVmCase
org.zstack.test.integration.kvm.status.DBOnlyCase
org.zstack.test.integration.kvm.capacity.CheckHostCapacityWhenAddHostCase

使用-Dapipath参数打印API调用的call graph

在运行一个测试用例时指定-Dapipath参数可以打印出用例运行中所有API(不包含读API,例如query/get API)引发的消息和HTTP RPC call,从而对每个API的call graph有个大致的了解。例如:

mvn  test -Dtest=OneVmBasicLifeCycleCase -Dapipath

用例运行成功并退出后,call graph文件生成在zstack-integration-test-result/apipath目录:

[root@localhost:/root/zstack/test/zstack-integration-test-result/apipath]# ls
org_zstack_sdk_AddImageAction                         org_zstack_sdk_CreateDiskOfferingAction           org_zstack_sdk_DestroyVmInstanceAction
org_zstack_sdk_AddIpRangeAction                       org_zstack_sdk_CreateInstanceOfferingAction       org_zstack_sdk_RebootVmInstanceAction
org_zstack_sdk_AddKVMHostAction                       org_zstack_sdk_CreateL2NoVlanNetworkAction        org_zstack_sdk_RecoverVmInstanceAction
org_zstack_sdk_AddLocalPrimaryStorageAction           org_zstack_sdk_CreateL3NetworkAction              org_zstack_sdk_StartVmInstanceAction
org_zstack_sdk_AddSftpBackupStorageAction             org_zstack_sdk_CreateVirtualRouterOfferingAction  org_zstack_sdk_StopVmInstanceAction
org_zstack_sdk_AttachNetworkServiceToL3NetworkAction  org_zstack_sdk_CreateVmInstanceAction
org_zstack_sdk_CreateClusterAction                    org_zstack_sdk_CreateZoneAction

[root@localhost:/root/zstack/test/zstack-integration-test-result/apipath]# cat org_zstack_sdk_CreateVmInstanceAction 
(Message) org.zstack.header.vm.APICreateVmInstanceMsg --->
(Message) org.zstack.header.vm.StartNewCreatedVmInstanceMsg --->
(Message) org.zstack.header.allocator.DesignatedAllocateHostMsg --->
(Message) org.zstack.header.storage.primary.AllocatePrimaryStorageMsg --->
(Message) org.zstack.header.volume.CreateVolumeMsg --->
(Message) org.zstack.header.network.l3.AllocateIpMsg --->
(Message) org.zstack.header.volume.InstantiateRootVolumeMsg --->
(Message) org.zstack.header.storage.primary.InstantiateRootVolumeFromTemplateOnPrimaryStorageMsg --->
(Message) org.zstack.header.storage.primary.AllocatePrimaryStorageMsg --->
(Message) org.zstack.storage.backup.sftp.GetSftpBackupStorageDownloadCredentialMsg --->
(Message) org.zstack.network.service.virtualrouter.CreateVirtualRouterVmMsg --->
(Message) org.zstack.appliancevm.StartNewCreatedApplianceVmMsg --->
(Message) org.zstack.header.allocator.DesignatedAllocateHostMsg --->
(Message) org.zstack.header.storage.primary.AllocatePrimaryStorageMsg --->
(Message) org.zstack.header.volume.CreateVolumeMsg --->
(Message) org.zstack.header.network.l3.AllocateIpMsg --->
(Message) org.zstack.header.network.l3.AllocateIpMsg --->
(Message) org.zstack.header.volume.InstantiateRootVolumeMsg --->
(Message) org.zstack.header.storage.primary.InstantiateRootVolumeFromTemplateOnPrimaryStorageMsg --->
(Message) org.zstack.header.storage.primary.AllocatePrimaryStorageMsg --->
(Message) org.zstack.storage.backup.sftp.GetSftpBackupStorageDownloadCredentialMsg --->
(Message) org.zstack.header.vm.CreateVmOnHypervisorMsg --->
(HttpRPC) [url:http://localhost:8989/vm/start, cmd: org.zstack.kvm.KVMAgentCommands$StartVmCmd] --->
(Message) org.zstack.appliancevm.ApplianceVmRefreshFirewallMsg --->
(HttpRPC) [url:http://localhost:8989/appliancevm/refreshfirewall, cmd: org.zstack.appliancevm.ApplianceVmCommands$RefreshFirewallCmd] --->
(Message) org.zstack.appliancevm.ApplianceVmRefreshFirewallMsg --->
(HttpRPC) [url:http://localhost:8989/appliancevm/refreshfirewall, cmd: org.zstack.appliancevm.ApplianceVmCommands$RefreshFirewallCmd] --->
(HttpRPC) [url:http://localhost:8989/init, cmd: org.zstack.network.service.virtualrouter.VirtualRouterCommands$InitCommand] --->
(Message) org.zstack.header.vm.CreateVmOnHypervisorMsg --->
(HttpRPC) [url:http://localhost:8989/vm/start, cmd: org.zstack.kvm.KVMAgentCommands$StartVmCmd] --->
(Message) org.zstack.network.securitygroup.RefreshSecurityGroupRulesOnVmMsg

新的测试用例应该加到哪儿

新的测试用例都应该加到test/src/test/groovy/org/zstack/test/integration/目录,目前已定义如下几大类test suite:

  1. org.zstack.test.integration.configuration.ConfigurationTest.groovy:

    所有配置相关的测试,包括instance offering, disk offering,global config的通用API

  2. org.zstack.test.integration.kvm.KvmTest.groovy:

    所有跟zone、cluster、host、host allocator、vm相关的通用测试

  3. org.zstack.test.integration.network.NetworkTest.groovy:

    除网络服务外(例如eip)的所有跟l2、l3网络,ip range相关的测试

  4. org.zstack.test.integration.networkservice.provider.NetworkServiceProviderTest.groovy:

    所有跟网络服务(eip,dhcp等)相关的测试

  5. org.zstack.test.integration.storage.StorageTest.groovy:

    所有跟存储相关的测试,包括primary storage、backup storage、volume、volume snapshot

下图包含所有已定义测试目录分类:

└── org
    └── zstack
        └── test
            └── integration
                ├── configuration
                │   ├── ConfigurationTest.groovy
                │   ├── diskoffering
                │   └── instanceoffering
                ├── kvm
                │   ├── Env.groovy
                │   ├── hostallocator
                │   ├── KvmTest.groovy
                │   └── lifecycle
                │       └── OneVmBasicLifeCycleCase.groovy
                ├── network
                │   ├── l2network
                │   ├── l3network
                │   │   └── getfreeip
                │   │       ├── OneL3OneIpRangeNoIpUsed.groovy
                │   │       ├── OneL3OneIpRangeSomeIpUsed.groovy
                │   │       └── OneL3TwoIpRanges.groovy
                │   └── NetworkTest.groovy
                ├── networkservice
                │   └── provider
                │       ├── flat
                │       │   ├── dhcp
                │       │   │   └── OneVmDhcp.groovy
                │       │   ├── eip
                │       │   ├── Env.groovy
                │       │   └── userdata
                │       │       └── OneVmUserdata.groovy
                │       ├── NetworkServiceProviderTest.groovy
                │       ├── securitygroup
                │       └── virtualrouter
                │           ├── dhcp
                │           ├── dns
                │           ├── eip
                │           ├── lb
                │           ├── portforwarding
                │           ├── snat
                │           └── VirtualRouterProviderTest.groovy
                └── storage
                    ├── backup
                    │   ├── ceph
                    │   └── sftp
                    ├── primary
                    │   ├── ceph
                    │   ├── local
                    │   ├── nfs
                    │   └── smp
                    ├── StorageTest.groovy
                    ├── volume
                    └── volumesnapshot
Clone this wiki locally