-
Notifications
You must be signed in to change notification settings - Fork 395
管理节点基于模拟器的Integration Test框架
目录
- 前言
- 实例
- 依赖环境
- 编写测试用例
- Test Suite
-
Cookbooks
- 如何在env DSL定义资源的时候指定其UUID
- 如何进行级联创建
- 如何在获得env DSL中定义的资源的spec
- 如何获取已加载的组件
- DatabaseFacade.findByUuid() 快捷函数
- 如何清理加载的simulator/message的handler
- JSON快捷函数
- 应该在哪里修改Global Config
- 如何重建一个被删除的资源,该资源是用
environment()
构造的 - 获得一个资源的inventory对象
- 使用
retryInSecs
和retryInMillis
检验异步操作结果 - 查看失败case log
- 获取Test Suite测试用例列表
- 使用
-Dapipath
参数打印API调用的call graph - 新的测试用例应该加到哪儿
作为产品型的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,开发者的开发环境需要满足如下条件:
-
从github上获得一份ZStack源代码
-
系统中安装了Mariadb数据库(或mysql)并运行,且数据库root用户的密码为空
Integreation Test启动时会部署ZStack数据库,需要使用到数据库root用户,默认使用空密码,此项可以通过配置文件改变
-
系统中已安装了rabbitmq并运行,rabbitmq的guest用户使用默认密码
我们强烈建议开发者使用一个干净的CentOS作为开发环境,不要把Integration Test和运行ZStack的试验环境放在同一台机器,Integration Test运行时部署数据库的操作会导致试验环境的ZStack数据库丢失。
所有Integreation Test(除后文讲到的Test Suite)都继承SubCase
类,例如:
class OneVmBasicLifeCycleCase extends SubCase {
并实现4个抽象函数:
- setup:配置用例,主要用于加载运行用例需要用到的ZStack服务和组件
- environment: 构造测试环境,例如创建zone、cluster,添加host等操作
- test:执行具体测试代码
- clean:清理环境 (仅当该case在test suite中运行时执行,后文详述)
测试用例运行时,上述4个函数依次执行,任何一个环节出现错误则测试终止退出(case在test suite中运行时例外)。
ZStack采用Spring框架,通过XML文件配置和管理要加载的服务和组件。XML配置文件存在于两个目录:
-
conf: 包含所有ZStack组件的XML配置文件
-
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服务的增加,还会有新的内置函数加入。
绝大部分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语法中每个资源可以包含三种成员:
- 参数:用于创建该资源的参数,例如
name = "zone"
就指定了创建该zone的name参数 - 子资源:例如l2NoVlanNetwork包含在zone中,它就是zone的一个子资源
- 函数:通常用于引用其它资源或关联其它资源
当create()
函数调用时,测试框架会遍历env DSL定义的资源树,并调用相应资源的SDK API进行创建,例如zone就会使用SDK中的CreateZoneAction进行创建。所以env DSL实质是为不同资源在SDK中的Create Action的参数赋值。例如zone资源包含name
和description
两个参数就对应了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需要指定managementNetworkUuid
、publicNetworkUuid
、imageUuid
字段,我们用useManagementL3Network
、usePublicL3Network
、useImage
去引用名为pubL3三层网络和名为vr的镜像,它们都是virtualRouterOffering的被依赖资源。create()
函数遍历资源树时,会首先创建被依赖资源,例如这里会保证pubL3三层网络和vr镜像先于virtualRouterOffering之前创建,并且在创建virtualRouterOffering时自动为managementNetworkUuid
、publicNetworkUuid
、imageUuid
字段赋上相应资源的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
├── 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()
函数中,作为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。
命名规则如下:
- 通过package名描述测试资源的场景,例如
getfreeip
是l3network的一个场景,而org.zstack.test.integration.kvm.lifecycle
是kvm的lifecycle场景。每个新场景都需要创建一个新的子package。 - 通过class名描述部署环境,例如
OneL3OneIpRangeNoIpUsed
和OneVmBasicLifeCycleCase
都能表示大概的部署场景。 - 通过函数名描述测试的具体内容,例如
useIpRangeUuidWithStartBeyondTheEndIp
、testStopVm
。 - 如果名字太长,英语中的一些介词可以省略,例如
useIpRangeUuidWithStartBeyondTheEndIp
可以省掉with
、the
变成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()
}
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。
测试用例在验证测试结果的时候可以使用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) ~[?:?]
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对象。
当我们希望改变测试框架默认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 [:]
}
当我们不希望改变测试框架默认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
}
我们可以在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"
这个条件满足时,消息才会被捕获。
// 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()
函数中销毁在environment()
中构建的EnvSpec
对象,例如:
@Override
void clean() {
env.delete()
}
测试用例单独执行时
clean()
不会被调用,以留存数据库环境供手动分析
测试用例可以单独执行,也可以放在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的整体结构跟测试用例类似,不同点在于:
- test suite继承
Test
类,测试用例继承SubCase
类 - test suite的
environment()
通常为空,因为各测试用例会自己创建的EnvSpec
对象 - test suite在
test()
函数中通过runSubCases()
执行一组测试用例 - test suite没有
clean()
函数
测试用例必须保证跟test suite加载相同的服务和组件,以保证用例单独执行和在test suite中执行时ZStack运行的组件和服务完全相同。故测试用例应该跟test suite有相同 setup()
函数。
runSubCases()
运行时会自动搜索该test suite所在package以及子package的所有测试用例,无需程序员显示指定。
运行test suite方法跟运行单个测试用例一样:
mvn test -Dtest=GetFreeIpTest
可以使用-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格式
相同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对象。
可以通过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中定义的资源可以通过名字和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)
}
可以通过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:
// env为EnvSpec对象
env.cleanSimulatorAndMessageHandlers()
env.cleanSimulatorHandlers()
env.cleanAfterSimulatorHandlers()
env.cleanMessageHandlers()
可以直接使用json()
函数将json字符串转换成对象:
env.afterSimulator(FlatUserdataBackend.RELEASE_USER_DATA) { rsp, HttpEntity<String> e ->
cmd = json(e.body, FlatUserdataBackend.ReleaseUserdataCmd.class)
return rsp
}
当一个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()
构造的包含在EnvSpec
对象中的资源。当用例中后面的测试函数需要用到这些资源时,重建是件非常麻烦的事情,这时可以用EnvSpec.recreate()
函数重建该资源,例如:
@Override
void test() {
env.create {
testGCSuccess()
testGCCancelledAfterHostDeleted()
//这里 testGCCancelledAfterHostDeleted() 删除了名为kvm的host,我们
//用env.recreate()重建它供testGCCancelledAfterPrimaryStorageDeleted()使用
env.recreate("kvm")
testGCCancelledAfterPrimaryStorageDeleted()
}
}
EnvSpec.recreate()
会重建资源以及它的子资源。
可以直接通过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")
当某些操作异步执行时(例如删除虚拟机后,归还磁盘容量的就是异步操作),我们需要等待一段时间确保异步操作完成再检验结果,可以使用retryInXxx
函数不断检测异步操作是否完成,具体使用方式见下例:
boolean ret = retryInSecs(3, 1) {
// 在这里执行操作结果检测
// 检测成功返回true,则retryInSecs会直接返回true,表示检测成功;
// 返回false,retryInSecs会sleep指定interval后(第二个参数,这里为1s)后再次执行该检测函数。
// 如果在指定间隔时间(第一个参数,这里为3s)检测函数都返回false,retryInSecs返回false,表示检测失败
return true
}
同样可以用retryInMillis()
进行毫秒级的循环检测。
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时指定-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(不包含读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:
-
org.zstack.test.integration.configuration.ConfigurationTest.groovy:
所有配置相关的测试,包括instance offering, disk offering,global config的通用API
-
org.zstack.test.integration.kvm.KvmTest.groovy:
所有跟zone、cluster、host、host allocator、vm相关的通用测试
-
org.zstack.test.integration.network.NetworkTest.groovy:
除网络服务外(例如eip)的所有跟l2、l3网络,ip range相关的测试
-
org.zstack.test.integration.networkservice.provider.NetworkServiceProviderTest.groovy:
所有跟网络服务(eip,dhcp等)相关的测试
-
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