摘要:在這篇文章,筆者想談?wù)剬ψ詣踊瘻y試的一些想法。很多人以為是用于做單元測試的。當(dāng)然,這些測試都屬于集成測試。而單元測試以及如何在自己的應(yīng)用中編寫一套更強(qiáng)大的自動測試框架這類主題,之后有機(jī)會筆者會再與大家分享。
本文首發(fā)于泊浮目的專欄:https://segmentfault.com/blog...前言
筆者工作2年有余,剛開始實習(xí)的時候是不知道自動化測試這種神器的,在剛開始工作的時候往往苦于救火滅火再救火,搞的心力憔悴,一度懷疑猿生。實踐自動化測試后感覺生產(chǎn)力慢慢的解放了,那個時候搞的還是偏單機(jī)應(yīng)用,測試的Cover也是止步在單機(jī)應(yīng)用上。在接觸到了ZStack以后,由于其產(chǎn)品化的特性,對軟件質(zhì)量要求偏高,然作為一個典型的分布式系統(tǒng),測試的覆蓋率卻是較高的。在這篇文章,筆者想談?wù)剬ψ詣踊瘻y試的一些想法。
收益自動化測試的收益點很明顯,幾乎眾所周知:
保證軟件質(zhì)量,重復(fù)的活交給機(jī)器來做,避免繁瑣重復(fù)的手動測試,節(jié)省人力;
為重構(gòu)打下良好的基礎(chǔ):軟件內(nèi)部無論如何重構(gòu),對外部請求所返回的結(jié)果不應(yīng)該有所變化;
保證核心類庫的邏輯不遭受破壞,同時也可以作為使用的“樣本”,由于沒有業(yè)務(wù)邏輯的耦合,代碼顯得更加清楚,便于閱讀;
.....
難點既然收益這么高,為什么現(xiàn)實中自動化測試實施起來就像勞動人民愛勞動這句話一樣這么不現(xiàn)實呢?大概有這幾點:
對代碼架構(gòu)要求較高:能靈活測試(集測、單測)的代碼往往是松耦合的,但是松耦合程度的控制可不是一個簡單的問題;
開發(fā)者得夠“懶”:開發(fā)者得愿意做一勞永逸的事,而不是每次都手測一下;
項目負(fù)責(zé)人對自動化測試不重視,眼里只有交付;
.....
ZStack的自動化測試實踐ZStack的自動化測試是基于Junit使用Grovvy編寫的集成測試,在運行時會把依賴的Bean按需加載進(jìn)來并啟動一個JVM進(jìn)程,同時也會啟動一個基于Jetty的HTTPServer用于Mock Agent的行為。
很多人以為Junit是用于做單元測試的。其實并非如此,官網(wǎng)上的介紹是: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, HttpEntitye -> 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 e -> stopCmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StopVmCmd.class) return rsp } env.afterSimulator(KVMConstant.KVM_START_VM_PATH) { rsp, HttpEntity 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 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 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() } }
我們先從跳轉(zhuǎn)到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,并實現(xiàn)了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:構(gòu)建一個環(huán)境
test:用于跑Case本身
run:用于跑SubCase
clean:清理環(huán)境。這是SubCase必須關(guān)注的,不然會導(dǎo)致環(huán)境中含有臟數(shù)據(jù)
在Test中,我們也可以看到定義里幾個關(guān)鍵抽象函數(shù),用于定義一個Case的行為:
abstract void setup() abstract void environment() abstract void test()
所以一個Case必須實現(xiàn)Test中的接口以及Case中的clean方法。
一般在setup中,會將依賴的Bean按需加載進(jìn)來。這在前面提到過;而environment則會構(gòu)建出一個環(huán)境。Grovvy對DSL支持較好,所以整個環(huán)境的構(gòu)建代碼可讀性極強(qiáng),本質(zhì)上每個DSL都對應(yīng)了一個Spec,而Sepc對應(yīng)了一個ZStack的SDK創(chuàng)建調(diào)用——即XXXAction。而XXXAction則通過HTTP調(diào)用ZStack的API接口。
平時在測試中大家可能會為了Build一個環(huán)境直接對數(shù)據(jù)庫進(jìn)行操作。例如:
xxxRepo.save(new Object());
但在ZStack中并不是一個很好的方案——一個Iaas中的資源依賴及狀態(tài)變動的關(guān)系是錯綜復(fù)雜的,因此調(diào)用外部的API來創(chuàng)建資源是一個明智的選擇。同時也可以測試SDK和API的行為是否是期待的。
在clean中也是如此。會調(diào)用ZStack本身的Cascade邏輯進(jìn)行資源清理。打開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], ]
設(shè)置了對應(yīng)的createAction和deleteAction,用于清理環(huán)境時調(diào)用。這樣同時也對Cascade邏輯進(jìn)行了Cover。
利用松耦合進(jìn)行靈活的測試如果看過ZStack的Case,可以看到很多類似的方法:
env.afterSimulator
env.simulator
env.message
這幾個方法用來hook Message和HTTP Request。由于在ZStack中各個組件的通信都由Message來完成,對于Agent的請求則是統(tǒng)一通過HTTP來完成。這樣在TestCase就可以任意模擬任何組件及agent的狀態(tài),讓Case有極強(qiáng)的實用性——也保證了ManagentMent Node的邏輯健壯。
在Java Web應(yīng)用中的MockMvc實踐自動化測試ZStack的SDK本質(zhì)上是包裝了一層HTTP Path,利用通用的協(xié)議便于開發(fā)者進(jìn)行開發(fā)或測試。而在傳統(tǒng)的Java WEB應(yīng)用中,一般會通過MockMvc進(jìn)行測試。其本質(zhì)也是通過調(diào)用每個API的Path傳參來進(jìn)行測試。接下來來看一個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.用戶登錄,測試自己是否處于登錄狀態(tài),并執(zhí)行更新信息、修改密碼操作 * 3.用戶登出,更新信息、在線修改密碼,應(yīng)全部失敗。 * 4.用戶用新信息登錄,成功 * 5.用戶登出,測試自己是否處于登錄狀態(tài),走忘記密碼流程 * 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 = "[email protected]"; 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 = "[email protected]"; 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 = "[email protected]"; 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的鏈?zhǔn)秸{(diào)用讓代碼可讀性變得極強(qiáng):
MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/user/register.do") .contentType(MediaType.APPLICATION_JSON) .content(registerJson) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn();
在這里,我們對MockMvc的對象設(shè)置了相應(yīng)的URL以及Content類型、數(shù)據(jù),并且期待了它的狀態(tài)碼。
小結(jié)在這篇文章中,筆者和大家一起分析了ZStack的自動化測試,以及在JavaWeb應(yīng)用中常見的測試方法。當(dāng)然,這些測試都屬于集成測試。而單元測試以及如何在自己的應(yīng)用中編寫一套更強(qiáng)大的自動測試框架這類主題,之后有機(jī)會筆者會再與大家分享。
擴(kuò)展閱讀:ZStack:管理節(jié)點基于模擬器的Integration Test框架
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/68384.html
摘要:本文首發(fā)于泊浮目的專欄在前文源碼剖析之二次開發(fā)可擴(kuò)展框架中,我們大概的了解了如何在中進(jìn)行二次開發(fā)。在還有相關(guān)的日志,有興趣的讀者可以自行搜索。掛斷點在掛斷點之前,請確定自己的開放了相應(yīng)的端口。之后記得使用關(guān)掉。 本文首發(fā)于泊浮目的專欄:https://segmentfault.com/blog... 在前文 ZStack源碼剖析之二次開發(fā)——可擴(kuò)展框架中,我們大概的了解了如何在ZSt...
摘要:對于日志級別的分類,有以下參考表示需要立即被處理的系統(tǒng)級錯誤。注意日志的切片設(shè)置,以免文件過大不方便操作。過期的日志定期清理。 本文首發(fā)于泊浮目的專欄:https://segmentfault.com/blog... 前言 前陣子在論壇上看到一個問題,大致的意思就是日志該怎么打?或者說是,我們應(yīng)該在日志中打印出哪些內(nèi)容? 看了該問題,筆者真是有所感慨:從實習(xí)到現(xiàn)在,從接的外包小項目到...
閱讀 3659·2021-10-09 09:58
閱讀 1202·2021-09-22 15:20
閱讀 2503·2019-08-30 15:54
閱讀 3520·2019-08-30 14:08
閱讀 897·2019-08-30 13:06
閱讀 1827·2019-08-26 12:16
閱讀 2687·2019-08-26 12:11
閱讀 2517·2019-08-26 10:38