技巧:ZStack如何做Integration Test

本文首发于泊浮目的专栏:https://segmentfault.com/blog/camile

版本 日期 备注
1.0 2018.2.1 文章首发
1.1 2021.5.21 修改标题:小谈自动化测试:从ZStack Integration Test谈起-> 技巧:ZStack如何做Integration Test

前言

笔者工作2年有余,刚开始实习的时候是不知道自动化测试这种神器的,在刚开始工作的时候往往苦于救火灭火再救火,搞的心力憔悴,一度怀疑猿生。实践自动化测试后感觉生产力慢慢的解放了,那个时候搞的还是偏单机应用,测试的Cover也是止步在单机应用上。在接触到了ZStack以后,由于其产品化的特性,对软件质量要求偏高,然作为一个典型的分布式系统,测试的覆盖率却是较高的。在这篇文章,笔者想谈谈对自动化测试的一些想法。

收益

自动化测试的收益点很明显,几乎众所周知:

  • 保证软件质量,重复的活交给机器来做,避免繁琐重复的手动测试,节省人力;
  • 为重构打下良好的基础:软件内部无论如何重构,对外部请求所返回的结果不应该有所变化;
  • 保证核心类库的逻辑不遭受破坏,同时也可以作为使用的“样本”,由于没有业务逻辑的耦合,代码显得更加清楚,便于阅读;
  • .....

难点

既然收益这么高,为什么现实中自动化测试实施起来就像劳动人民爱劳动这句话一样这么不现实呢?大概有这几点:

  • 对代码架构要求较高:能灵活测试(集测、单测)的代码往往是松耦合的,但是松耦合程度的控制可不是一个简单的问题;
  • 开发者得够“懒”:开发者得愿意做一劳永逸的事,而不是每次都手测一下;
  • 项目负责人对自动化测试不重视,眼里只有交付;
  • .....

ZStack的自动化测试实践

ZStack的自动化测试是基于Junit使用Grovvy编写的集成测试,在运行时会把依赖的Bean按需加载进来并启动一个JVM进程,同时也会启动一个基于Jetty的HTTPServer用于Mock Agent的行为。

很多人以为Junit是用于做单元测试的。其实并非如此,官网上的介绍是:JUnit is a simple framework to write repeatable tests. It is an instance of the xUnit architecture for unit testing frameworks.

从代码说起

package org.zstack.test.integration.kvm.vm

import org.springframework.http.HttpEntity
import org.zstack.header.vm.VmCreationStrategy
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.CreateVmInstanceAction
import org.zstack.sdk.DiskOfferingInventory
import org.zstack.sdk.ImageInventory
import org.zstack.sdk.InstanceOfferingInventory
import org.zstack.sdk.L3NetworkInventory
import org.zstack.sdk.VmInstanceInventory
import org.zstack.test.integration.kvm.Env
import org.zstack.test.integration.kvm.KvmTest
import org.zstack.testlib.EnvSpec
import org.zstack.testlib.SubCase
import org.zstack.testlib.VmSpec
import org.zstack.utils.gson.JSONObjectUtil

/**
 * Created by xing5 on 2017/2/22.
 */
class OneVmBasicLifeCycleCase extends SubCase {
    EnvSpec env

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

    @Override
    void setup() {
        useSpring(KvmTest.springSpec)
    }

    @Override
    void environment() {
        env = Env.oneVmBasicEnv()
    }

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

    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.rootVolume.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
    }

    void testDeleteCreatedVm() {
        VmSpec spec = env.specByName("vm")
        DiskOfferingInventory diskOfferingInventory = env.inventoryByName("diskOffering")
        InstanceOfferingInventory instanceOfferingInventory = env.inventoryByName("instanceOffering")
        ImageInventory imageInventory = env.inventoryByName("image1")
        L3NetworkInventory l3NetworkInventory = env.inventoryByName("l3")

        CreateVmInstanceAction action = new CreateVmInstanceAction()
        action.name = "JustCreatedVm"
        action.rootDiskOfferingUuid = diskOfferingInventory.uuid
        action.instanceOfferingUuid = instanceOfferingInventory.uuid
        action.imageUuid = imageInventory.uuid
        action.l3NetworkUuids = [l3NetworkInventory.uuid]
        action.strategy = VmCreationStrategy.JustCreate.toString()
        action.sessionId = adminSession()
        CreateVmInstanceAction.Result result = action.call()

        destroyVmInstance {
            uuid = result.value.inventory.uuid
        }

        VmInstanceVO vo = dbFindByUuid(result.value.inventory.uuid, VmInstanceVO.class)
        assert vo == null
    }

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

我们先从跳转到extends的SubCase中:

package org.zstack.testlib

/**
 * Created by xing5 on 2017/2/22.
 */
abstract class SubCase extends Test implements Case {
    final void run() {
        try {
            environment()
            test()
        } catch (Throwable t) {
            logger.warn("a sub case [${this.class}] fails, ${t.message}", t)
            collectErrorLog()
            throw t
        } finally {
            logger.info("start cleanup for case ${this.class}")
            try{
                clean()
            }catch (Throwable t){
                collectErrorLog()
                throw t
            }
        }
    }

    @Override
    protected void runSubCases() {
        throw new Exception("runSubCases() cannot be called in a SubCase")
    }
}

从签名中可以看到,其继承于Test,并实现了Case接口中的方法,我们看一下 Case

package org.zstack.testlib

/**
 * Created by xing5 on 2017/3/3.
 */
interface Case {
    void environment()
    void test()
    void run()
    void clean()
}

这里定义一个SubCase的基本行为:

  • environment:构建一个环境
  • test:用于跑Case本身
  • run:用于跑SubCase
  • clean:清理环境。这是SubCase必须关注的,不然会导致环境中含有脏数据

Test中,我们也可以看到定义里几个关键抽象函数,用于定义一个Case的行为:

    abstract void setup()
    abstract void environment()
    abstract void test()

所以一个Case必须实现Test中的接口以及Case中的clean方法。

一般在setup中,会将依赖的Bean按需加载进来。这在前面提到过;而environment则会构建出一个环境。Grovvy对DSL支持较好,所以整个环境的构建代码可读性极强,本质上每个DSL都对应了一个Spec,而Sepc对应了一个ZStack的SDK创建调用——即XXXAction。而XXXAction则通过HTTP调用ZStack的API接口。

平时在测试中大家可能会为了Build一个环境直接对数据库进行操作。例如:

xxxRepo.save(new Object());

但在ZStack中并不是一个很好的方案——一个Iaas中的资源依赖及状态变动的关系是错综复杂的,因此调用外部的API来创建资源是一个明智的选择。同时也可以测试SDK和API的行为是否是期待的。

在clean中也是如此。会调用ZStack本身的Cascade逻辑进行资源清理。打开EnvSpec.Grovvy可以看到

static List deletionMethods = [
            [CreateZoneAction.metaClass, CreateZoneAction.Result.metaClass, DeleteZoneAction.class],
            [AddCephBackupStorageAction.metaClass, AddCephBackupStorageAction.Result.metaClass, DeleteBackupStorageAction.class],
            [AddCephPrimaryStorageAction.metaClass, AddCephPrimaryStorageAction.Result.metaClass, DeletePrimaryStorageAction.class],
            [AddCephPrimaryStoragePoolAction.metaClass, AddCephPrimaryStoragePoolAction.Result.metaClass, DeleteCephPrimaryStoragePoolAction.class],
            [CreateEipAction.metaClass, CreateEipAction.Result.metaClass, DeleteEipAction.class],
            [CreateClusterAction.metaClass, CreateClusterAction.Result.metaClass, DeleteClusterAction.class],
            [CreateDiskOfferingAction.metaClass, CreateDiskOfferingAction.Result.metaClass, DeleteDiskOfferingAction.class],
            [CreateInstanceOfferingAction.metaClass, CreateInstanceOfferingAction.Result.metaClass, DeleteInstanceOfferingAction.class],
            [CreateAccountAction.metaClass, CreateAccountAction.Result.metaClass, DeleteAccountAction.class],
            [CreatePolicyAction.metaClass, CreatePolicyAction.Result.metaClass, DeletePolicyAction.class],
            [CreateUserGroupAction.metaClass, CreateUserGroupAction.Result.metaClass, DeleteUserGroupAction.class],
            [CreateUserAction.metaClass, CreateUserAction.Result.metaClass, DeleteUserAction.class],
            [AddImageAction.metaClass, AddImageAction.Result.metaClass, DeleteImageAction.class],
            [CreateDataVolumeTemplateFromVolumeAction.metaClass, CreateDataVolumeTemplateFromVolumeAction.Result.metaClass, DeleteImageAction.class],
            [CreateRootVolumeTemplateFromRootVolumeAction.metaClass, CreateRootVolumeTemplateFromRootVolumeAction.Result.metaClass, DeleteImageAction.class],
            [CreateL2NoVlanNetworkAction.metaClass, CreateL2NoVlanNetworkAction.Result.metaClass, DeleteL2NetworkAction.class],
            [CreateL2VlanNetworkAction.metaClass, CreateL2VlanNetworkAction.Result.metaClass, DeleteL2NetworkAction.class],
            [AddIpRangeByNetworkCidrAction.metaClass, AddIpRangeByNetworkCidrAction.Result.metaClass, DeleteIpRangeAction.class],
            [CreateL3NetworkAction.metaClass, CreateL3NetworkAction.Result.metaClass, DeleteL3NetworkAction.class],
            [CreateSchedulerJobAction.metaClass, CreateSchedulerJobAction.Result.metaClass, DeleteSchedulerJobAction.class],
            [CreateSchedulerTriggerAction.metaClass, CreateSchedulerTriggerAction.Result.metaClass, DeleteSchedulerTriggerAction.class],
            [CreateVmInstanceAction.metaClass, CreateVmInstanceAction.Result.metaClass, DestroyVmInstanceAction.class],
            [CreateDataVolumeFromVolumeSnapshotAction.metaClass, CreateDataVolumeFromVolumeSnapshotAction.Result.metaClass, DeleteDataVolumeAction.class],
            [CreateDataVolumeFromVolumeTemplateAction.metaClass, CreateDataVolumeFromVolumeTemplateAction.Result.metaClass, DeleteDataVolumeAction.class],
            [CreateDataVolumeAction.metaClass, CreateDataVolumeAction.Result.metaClass, DeleteDataVolumeAction.class],
            [CreateVolumeSnapshotAction.metaClass, CreateVolumeSnapshotAction.Result.metaClass, DeleteVolumeSnapshotAction.class],
            [AddKVMHostAction.metaClass, AddKVMHostAction.Result.metaClass, DeleteHostAction.class],
            [CreateLoadBalancerAction.metaClass, CreateLoadBalancerAction.Result.metaClass, DeleteLoadBalancerAction.class],
            [AddLocalPrimaryStorageAction.metaClass, AddLocalPrimaryStorageAction.Result.metaClass, DeletePrimaryStorageAction.class],
            [AddImageStoreBackupStorageAction.metaClass, AddImageStoreBackupStorageAction.Result.metaClass, DeleteBackupStorageAction.class],
            [AddNfsPrimaryStorageAction.metaClass, AddNfsPrimaryStorageAction.Result.metaClass, DeletePrimaryStorageAction.class],
            [CreatePortForwardingRuleAction.metaClass, CreatePortForwardingRuleAction.Result.metaClass, DeletePortForwardingRuleAction.class],
            [CreateSecurityGroupAction.metaClass, CreateSecurityGroupAction.Result.metaClass, DeleteSecurityGroupAction.class],
            [AddSftpBackupStorageAction.metaClass, AddSftpBackupStorageAction.Result.metaClass, DeleteBackupStorageAction.class],
            [AddSharedMountPointPrimaryStorageAction.metaClass, AddSharedMountPointPrimaryStorageAction.Result.metaClass, DeletePrimaryStorageAction.class],
            [CreateVipAction.metaClass, CreateVipAction.Result.metaClass, DeleteVipAction.class],
            [CreateVirtualRouterOfferingAction.metaClass, CreateVirtualRouterOfferingAction.Result.metaClass, DeleteInstanceOfferingAction.class],
            [CreateWebhookAction.metaClass, CreateWebhookAction.Result.metaClass, DeleteWebhookAction.class],
            [CreateBaremetalPxeServerAction.metaClass, CreateBaremetalPxeServerAction.Result.metaClass, DeleteBaremetalPxeServerAction.class],
            [CreateBaremetalChassisAction.metaClass, CreateBaremetalChassisAction.Result.metaClass, DeleteBaremetalChassisAction.class],
            [CreateBaremetalHostCfgAction.metaClass, CreateBaremetalHostCfgAction.Result.metaClass, DeleteBaremetalHostCfgAction.class],
            [CreateMonitorTriggerAction.metaClass, CreateMonitorTriggerAction.Result.metaClass, DeleteMonitorTriggerAction.class],
            [CreateEmailMonitorTriggerActionAction.metaClass, CreateEmailMonitorTriggerActionAction.Result.metaClass, DeleteMonitorTriggerActionAction.class],
            [CreateEmailMediaAction.metaClass, CreateEmailMediaAction.Result.metaClass, DeleteMediaAction.class],
            [AddLdapServerAction.metaClass, AddLdapServerAction.Result.metaClass, DeleteLdapServerAction.class],
            [SubmitLongJobAction.metaClass, SubmitLongJobAction.Result.metaClass, DeleteLongJobAction.class],
    ]

设置了对应的createAction和deleteAction,用于清理环境时调用。这样同时也对Cascade逻辑进行了Cover。

利用松耦合进行灵活的测试

如果看过ZStack的Case,可以看到很多类似的方法:

  • env.afterSimulator
  • env.simulator
  • env.message

这几个方法用来hook Message和HTTP Request。由于在ZStack中各个组件的通信都由Message来完成,对于Agent的请求则是统一通过HTTP来完成。这样在TestCase就可以任意模拟任何组件及agent的状态,让Case有极强的实用性——也保证了ManagentMent Node的逻辑健壮。

在Java Web应用中的MockMvc实践自动化测试

ZStack的SDK本质上是包装了一层HTTP Path,利用通用的协议便于开发者进行开发或测试。而在传统的Java WEB应用中,一般会通过MockMvc进行测试。其本质也是通过调用每个API的Path传参来进行测试。接下来来看一个demo:

import com.camile.base.Utils.JsonUtils;
import com.camile.base.common.CommonResponse;
import com.camile.base.common.error.ResponseCode;
import com.camile.base.common.utils.MD5Util;
import com.camile.base.data.dao.UserRepository;
import com.camile.base.data.dto.user.*;
import com.camile.base.data.entity.UserEntity;
import com.camile.base.data.vo.UserVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.http.HttpSession;
import java.util.Map;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
 * Created by Camile 
 * 1.用户注册
 * 2.用户登录,测试自己是否处于登录状态,并执行更新信息、修改密码操作
 * 3.用户登出,更新信息、在线修改密码,应全部失败。
 * 4.用户用新信息登录,成功
 * 5.用户登出,测试自己是否处于登录状态,走忘记密码流程
 * 6.修改后再次登录,成功
 */
@Slf4j
@Transactional
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class UserBasicTests {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private WebApplicationContext context;
    private String uuid;
    private HttpSession session;
    private MockMvc mvc;
    private ObjectMapper mapper;
    private final String email = "487643862@qq.com";
    private String password = "newPassword";
    private final String question = "are you ok ?";
    private final String answer = "im fine";
    private final String name = "camile";
    private final String phone = "13043769014";
    private String updateName = "camile1";
    private String updateEmail = "587643862@qq.com";
    private String updatePhone = "13834671096";
    @Before
    public void setUp() throws Exception {
        mvc = MockMvcBuilders.webAppContextSetup(this.context).build();
        mapper = new ObjectMapper();
    }
    @Test
    public void test() throws Exception {
        testRegisterSuccess();
        testIsLoginFailure();
        testLoginSuccess();
        testIsLoginSuccess();
        testUpdateInformationSuccess();
        testOnlineRestPwdSuccess();
        testLoginOutSuccess();
        testUpdateInformationFailure();
        testOnlineRestPwdFailure();
        testloginWithOldPwdFailure();
        testLoginWithNewInfoSuccess();
        testLoginOutSuccess();
        testForgetPwdAndResetSuccess();
        testLoginWithNewInfoSuccess();
    }
    private void testRegisterSuccess() throws Exception {
        UserAllPropertyDTO dto = new UserAllPropertyDTO();
        dto.setEmail(email);
        dto.setPassword(password);
        dto.setQuestion(question);
        dto.setAnswer(answer);
        dto.setName(name);
        dto.setPhone(phone);
        String registerJson = JsonUtils.ObjectToJson(dto);
        MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/user/register.do")
                .contentType(MediaType.APPLICATION_JSON)
                .content(registerJson)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        CommonResponse response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(response.getCode(), ResponseCode.Success.getCode());
        UserVO vo = JsonUtils.jsonToObject((Map) response.getData(), UserVO.class);
        Assert.assertNotNull(userRepository.findByUuid(vo.getUuid()));
        uuid = vo.getUuid();
        session = result.getRequest().getSession();
    }
    private void testIsLoginFailure() throws Exception { // never login
        MvcResult result = mvc.perform(MockMvcRequestBuilders.get(String.format("/user/isLogin.do?uuid=%s", uuid))
                .session((MockHttpSession) session)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        CommonResponse response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(response.getCode(), ResponseCode.NeedLogin.getCode());
        session = result.getRequest().getSession();
    }
    private void testLoginSuccess() throws Exception {
        UserLoginDTO dto = new UserLoginDTO(name, password);
        String loginJson = JsonUtils.ObjectToJson(dto);
        MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/user/login.do")
                .session((MockHttpSession) session)
                .contentType(MediaType.APPLICATION_JSON)
                .content(loginJson)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        CommonResponse response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(response.getCode(), ResponseCode.Success.getCode());
        session = result.getRequest().getSession();
    }
    private void testIsLoginSuccess() throws Exception {
        MvcResult result = mvc.perform(MockMvcRequestBuilders.get(String.format("/user/isLogin.do?uuid=%s", uuid))
                .session((MockHttpSession) session)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        CommonResponse response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(ResponseCode.Success.getCode(), response.getCode());
        session = result.getRequest().getSession();
    }
    private void testUpdateInformationSuccess() throws Exception {
        UserDTO dto = new UserDTO();
        dto.setUuid(uuid);
        dto.setName(updateName);
        dto.setEmail(updateEmail);
        dto.setPhone(updatePhone);
        String updateJson = JsonUtils.ObjectToJson(dto);
        MvcResult result = mvc.perform(MockMvcRequestBuilders.put("/user/information.do")
                .session((MockHttpSession) session)
                .contentType(MediaType.APPLICATION_JSON)
                .content(updateJson)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        CommonResponse response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(response.getMsg(), ResponseCode.Success.getCode(), response.getCode());
        UserEntity entity = userRepository.findByUuid(uuid);
        UserVO vo = JsonUtils.jsonToObject((Map) response.getData(), UserVO.class);
        Assert.assertNotNull(entity);
        Assert.assertEquals(vo.getName(), entity.getName());
        Assert.assertEquals(vo.getPhone(), entity.getPhone());
        Assert.assertEquals(vo.getEmail(), entity.getEmail());
        Assert.assertEquals(vo.getEmail(), updateEmail);
        Assert.assertEquals(vo.getPhone(), updatePhone);
        Assert.assertEquals(vo.getName(), updateName);
        session = result.getRequest().getSession();
    }
    private void testOnlineRestPwdSuccess() throws Exception {
        UserResetPwdDTO dto = new UserResetPwdDTO();
        dto.setUuid(uuid);
        dto.setOldPassword(password);
        dto.setNewPassword("12345678");
        password = "12345678";
        String resetPwdJson = JsonUtils.ObjectToJson(dto);
        MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/user/onlineResetPwd.do")
                .session((MockHttpSession) session)
                .contentType(MediaType.APPLICATION_JSON)
                .content(resetPwdJson)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        CommonResponse response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(response.getMsg(), response.getCode(), ResponseCode.Success.getCode());
        session = result.getRequest().getSession();
        UserEntity userEntity = userRepository.findByUuid(uuid);
        Assert.assertEquals(userEntity.getPassword(), MD5Util.MD5EncodeUtf8(password));
    }
    private void testLoginOutSuccess() throws Exception {
        MvcResult result = mvc.perform(MockMvcRequestBuilders.post(String.format("/user/loginOut.do?uuid=%s", uuid))
                .session((MockHttpSession) session)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        CommonResponse response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(response.getCode(), ResponseCode.Success.getCode());
        session = result.getRequest().getSession();
    }
    private void testUpdateInformationFailure() throws Exception {
        String updateName = "camile2";
        String updateEmail = "687643862@qq.com";
        String updatePhone = "14834671096";
        UserDTO dto = new UserDTO();
        dto.setUuid(uuid);
        dto.setName(updateName);
        dto.setEmail(updateEmail);
        dto.setPhone(updatePhone);
        String updateJson = JsonUtils.ObjectToJson(dto);
        MvcResult result = mvc.perform(MockMvcRequestBuilders.put("/user/information.do")
                .session((MockHttpSession) session)
                .contentType(MediaType.APPLICATION_JSON)
                .content(updateJson)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        CommonResponse response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(response.getMsg(), ResponseCode.Failure.getCode(), response.getCode());
        session = result.getRequest().getSession();
    }
    private void testOnlineRestPwdFailure() throws Exception {
        UserResetPwdDTO dto = new UserResetPwdDTO();
        dto.setUuid(uuid);
        dto.setOldPassword(password);
        dto.setNewPassword("123456789");
        String resetPwdJson = JsonUtils.ObjectToJson(dto);
        MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/user/onlineResetPwd.do")
                .session((MockHttpSession) session)
                .contentType(MediaType.APPLICATION_JSON)
                .content(resetPwdJson)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        CommonResponse response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(response.getMsg(), response.getCode(), ResponseCode.Failure.getCode());
        session = result.getRequest().getSession();
        UserEntity userEntity = userRepository.findByUuid(uuid);
        Assert.assertNotEquals(userEntity.getPassword(), MD5Util.MD5EncodeUtf8("123456789"));
    }
    private void testloginWithOldPwdFailure() throws Exception {
        UserLoginDTO dto = new UserLoginDTO(name, "newPassword");
        String loginJson = JsonUtils.ObjectToJson(dto);
        MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/user/login.do")
                .session((MockHttpSession) session)
                .contentType(MediaType.APPLICATION_JSON)
                .content(loginJson)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        CommonResponse response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(response.getCode(), ResponseCode.UserInfoError.getCode());
        session = result.getRequest().getSession();
    }
    private void testLoginWithNewInfoSuccess() throws Exception {
        UserLoginDTO dto = new UserLoginDTO(updateName, password);
        String loginJson = JsonUtils.ObjectToJson(dto);
        MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/user/login.do")
                .session((MockHttpSession) session)
                .contentType(MediaType.APPLICATION_JSON)
                .content(loginJson)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        CommonResponse response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(ResponseCode.Success.getCode(), response.getCode());
        session = result.getRequest().getSession();
    }
    private void testForgetPwdAndResetSuccess() throws Exception {
        MvcResult result = mvc.perform(MockMvcRequestBuilders.get(String.format("/user/forget/question?name=%s", updateName))
                .session((MockHttpSession) session)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        CommonResponse response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(response.getMsg(), ResponseCode.Success.getCode(), response.getCode());
        session = result.getRequest().getSession();
        String question = (String) response.getData();
        Assert.assertEquals(question, this.question);
        UserQuestionDTO dto = new UserQuestionDTO();
        dto.setName(updateName);
        dto.setQuestion(question);
        dto.setAnswer(answer);
        String questionJson = JsonUtils.ObjectToJson(dto);
        result = mvc.perform(MockMvcRequestBuilders.post("/user/forget/checkAnswer.do")
                .session((MockHttpSession) session)
                .contentType(MediaType.APPLICATION_JSON)
                .content(questionJson)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(ResponseCode.Success.getCode(), response.getCode());
        session = result.getRequest().getSession();
        String token = (String) response.getData();
        UserForgetResetPwdDTO userForgetResetPwdDTO = new UserForgetResetPwdDTO();
        userForgetResetPwdDTO.setForgetToken(token);
        userForgetResetPwdDTO.setName(updateName);
        userForgetResetPwdDTO.setNewPassword("superpwd!");
        password = "superpwd!";
        String resetPwdDTO = JsonUtils.ObjectToJson(userForgetResetPwdDTO);
        result = mvc.perform(MockMvcRequestBuilders.post("/user/forget/resetPassword.do")
                .session((MockHttpSession) session)
                .contentType(MediaType.APPLICATION_JSON)
                .content(resetPwdDTO)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        response = mapper.readValue(content, CommonResponse.class);
        Assert.assertEquals(response.getMsg(), ResponseCode.Success.getCode(), response.getCode());
        session = result.getRequest().getSession();
        UserEntity userEntity = userRepository.findByUuid(uuid);
        Assert.assertEquals(userEntity.getPassword(), MD5Util.MD5EncodeUtf8(password));
    }
}

我们可以看到MockMvc的链式调用让代码可读性变得极强:

       MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/user/register.do")
                .contentType(MediaType.APPLICATION_JSON)
                .content(registerJson)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();

在这里,我们对MockMvc的对象设置了相应的URL以及Content类型、数据,并且期待了它的状态码。

小结

在这篇文章中,笔者和大家一起分析了ZStack的自动化测试,以及在JavaWeb应用中常见的测试方法。当然,这些测试都属于集成测试。而单元测试以及如何在自己的应用中编写一套更强大的自动测试框架这类主题,之后有机会笔者会再与大家分享。

扩展阅读:ZStack:管理节点基于模拟器的Integration Test框架

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容