2.5单元测试示例
单元测试常见问题
数据源不独立,常因为新增加的测试用例,影响到以前的测试用例
期待结果写在测试代码中,耦合度高
每次增加测试用例,都需要更改测试代码,测试代码不可复用
本小节中,我们将介绍一种优雅的测试用例方式
我们会使用一个json文件囊括测试用例所需的所有入参,依赖,期待结果,期待异常。从而没次增加测试用例,只需要向json文件中添加数据即可,无需改动代码
每一个方法拥有独立的数据源
实体类
com.moluo.example.util.TestCase
package com.inspur.securityhubapi.util;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
/**
* 测试用例类
* <p>
* 使用此类,json结构体需要满足如下格式
* <pre>
* {
* "方法名": [
* {
* "description": "",
* "args": {
* JsonObject对象
* },
* "relyMethodReturnMocks": {
* JsonArray对象
* },
* "exceptSuccessResults": {
* JsonArray对象
* },
* "exceptFailureResults": {
* JsonArray对象
* }
* }
* ]
* }
* </pre>
*
* @author moluo
* @since 2020/5/22
*/
public class TestCase {
private String description;
private JSONObject args;
private JSONObject relyMethodReturnMocks;
private JSONObject exceptSuccessResults;
private JSONObject exceptFailureResults;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public <T> T getArg(String key, Class<T> clazz) {
return args.getObject(key, clazz);
}
public void setArgs(JSONObject args) {
this.args = args;
}
public void addArg(String key, JSONObject jsonObject) {
args.put(key, jsonObject);
}
public <T> T getRelyMethodReturnMock(String key, int index, Class<T> clazz) {
return relyMethodReturnMocks.getJSONArray(key).getObject(index, clazz);
}
public void setRelyMethodReturnMocks(JSONObject relyMethodReturnMocks) {
this.relyMethodReturnMocks = relyMethodReturnMocks;
}
public void addRelyMethodReturnMock(String key, JSONArray values) {
relyMethodReturnMocks.put(key, values);
}
public <T> T getExceptSuccessResult(String key, int index, Class<T> clazz) {
return exceptSuccessResults.getJSONArray(key).getObject(index, clazz);
}
public void setExceptSuccessResults(JSONObject exceptSuccessResults) {
this.exceptSuccessResults = exceptSuccessResults;
}
public void addExceptSuccessResult(String key, JSONArray values) {
exceptSuccessResults.put(key, values);
}
public Boolean hasExceptSuccessResults() {
return !exceptSuccessResults.isEmpty();
}
public <T> T getExceptFailureResult(String key, int index, Class<T> clazz) {
return exceptFailureResults.getJSONArray(key).getObject(index, clazz);
}
public void setExceptFailureResults(JSONObject exceptFailureResults) {
this.exceptFailureResults = exceptFailureResults;
}
public void addExceptFailureResult(String key, JSONArray values) {
exceptFailureResults.put(key, values);
}
public Boolean hasExceptFailureResults() {
return !exceptFailureResults.isEmpty();
}
}
需要的工具类
com.moluo.example.util.JsonUtils
package com.inspur.securityhubapi.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.parser.Feature;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* JSON文件读取工具类
*
* @author moluo
* @since 2019/8/2
*/
public class JsonUtils {
/**
* 读取指定路径的文件并转化为指定对象列表
*
* @param path 文件路径
* @param clazz 列表item对象类型
* @param <T> 泛型
* @return 指定类型对象列表
*/
public static <T> List<T> readObjects(String path, Class<T> clazz) {
String s = readObject(path, String.class);
return JSONArray.parseArray(s, clazz);
}
/**
* 读取指定路径的文件并转化为指定对象
* <p>
* 该方法对{@link JsonUtils#readJsonFromClassPath(String, Type)}进行简单封装,捕获其抛出的异常
*
* @param path 文件路径
* @param type 类型对象
* @param <T> 泛型
* @return 指定类型的对象
*/
public static <T> T readObject(String path, Type type) {
try {
return readJsonFromClassPath(path, type);
} catch (IOException e) {
System.err.println("cannot find file " + path);
e.printStackTrace();
}
return null;
}
/**
* 转换jsonArray为int数组
*
* @param jsonArray JSONArray对象
* @return int数组
*/
public static int[] jsonArrayToIntArray(JSONArray jsonArray) {
int[] results = new int[jsonArray.size()];
for (int i = 0; i < jsonArray.size(); i++) {
results[i] = jsonArray.getIntValue(i);
}
return results;
}
/**
* 读取指定路径的文件并转化为指定对象
*
* @param path 文件路径
* @param type 类型对象
* @param <T> 泛型
* @return 指定类型的对象
* @throws IOException IO异常
*/
private static <T> T readJsonFromClassPath(String path, Type type) throws IOException {
File file = new File("src/test/resources/" + path);
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if (fileInputStream == null) {
return null;
}
return JSON.parseObject(fileInputStream, StandardCharsets.UTF_8, type,
// 自动关闭流
Feature.AutoCloseSource,
// 允许注释
Feature.AllowComment,
// 允许单引号
Feature.AllowSingleQuotes,
// 使用 Big decimal
Feature.UseBigDecimal);
}
}
com.moluo.example.util.TestCaseUtils
package com.inspur.securityhubapi.util;
import com.alibaba.fastjson.JSONObject;
import java.util.ArrayList;
import java.util.List;
/**
* 测试用例工具类
* {@link TestCase}
*
* @author moluo
* @since 2020/5/22
*/
public class TestCaseUtils {
private String testMethodName;
private String testCaseJsonPath;
public TestCaseUtils(String testMethodName, String testCaseJsonPath) {
this.testMethodName = testMethodName;
this.testCaseJsonPath = testCaseJsonPath;
}
public List<TestCase> getTestCases() {
List<TestCase> result = new ArrayList<>();
JSONObject testJsonObject = JsonUtils.readObject(testCaseJsonPath, JSONObject.class);
List<JSONObject> testMethodJsons = testJsonObject.getJSONArray(testMethodName).toJavaList(JSONObject.class);
for (JSONObject json : testMethodJsons) {
TestCase testCase = JSONObject.toJavaObject(json, TestCase.class);
result.add(testCase);
}
return result;
}
}
示例json文件
resources/mock/json/mq/CsfMessageListenerTest.json
{
"createSecurityGroup": [
{
"description": "监听安全组创建事件-安全组已存在,跳过创建",
"args": {
"messageBody": {
"index": 1,
"requestId": "335f7af8-a900-4294-962f-06270481de53",
"return": {
"eventMessage": {},
"result": "{\"security_groups_man\":\"6226056e-a8fa-4132-8e19-ac2bb6d97f58\"}",
"status": "SKIPPED",
"error": ""
},
"serviceId": "iot-yf-TNemt3uqQf"
}
},
"relyMethodReturnMocks": {
"productInsts": [
{
"id": "4028121b724a842701724fbb78000fc2",
"name": "云堡垒机-安全组测试",
"description": ""
}
]
},
"exceptSuccessResults": {
"exceptMethodCallCounts": [
{
"bssMessageSender.senderCreateFailMessage": 0
}
]
},
"exceptFailureResults": {
}
},
{
"description": "监听安全组创建事件-安全组创建失败",
"args": {
"messageBody": {
"index": 1,
"requestId": "361050e7-314f-402d-9951-17f533bf28d4",
"return": {
"eventMessage": {},
"result": "{\"msg\":\"[]1 error(s) occurred:* openstack_networking_secgroup_v2.secgroup_eth1: 1 error(s) occurred:* openstack_networking_secgroup_v2.secgroup_eth1: Expected HTTP response code [201 202] when accessing [POST http://172.16.1.20:9696/v2.0/security-groups], but got 409 instead{\\\"NeutronError\\\": {\\\"message\\\": \\\"Quota exceeded for resources: ['security_group'].\\\", \\\"type\\\": \\\"OverQuota\\\", \\\"detail\\\": \\\"\\\"}}\"}",
"status": "FAILED",
"error": "{\"msg\":\"[]1 error(s) occurred:* openstack_networking_secgroup_v2.secgroup_eth1: 1 error(s) occurred:* openstack_networking_secgroup_v2.secgroup_eth1: Expected HTTP response code [201 202] when accessing [POST http://172.16.1.20:9696/v2.0/security-groups], but got 409 instead{\\\"NeutronError\\\": {\\\"message\\\": \\\"Quota exceeded for resources: ['security_group'].\\\", \\\"type\\\": \\\"OverQuota\\\", \\\"detail\\\": \\\"\\\"}}\"}"
},
"serviceId": "iot-yf-TNemt3uqQf"
}
},
"relyMethodReturnMocks": {
"productInsts": [
{
"id": "4028121b724a842701724fbb78000fc2",
"name": "云堡垒机-安全组测试",
"description": ""
}
]
},
"exceptSuccessResults": {
"exceptMethodCallCounts": [
{
"bssMessageSender.senderCreateFailMessage": 1
}
]
},
"exceptFailureResults": {
}
},
{
"description": "监听安全组创建事件-发生IO异常",
"args": {
"messageBody": {
"index": 1,
"requestId": "361050e7-314f-402d-9951-17f533bf28d4",
"return": {
"eventMessage": {},
"result": "{\"msg\":\"[]1 error(s) occurred:* openstack_networking_secgroup_v2.secgroup_eth1: 1 error(s) occurred:* openstack_networking_secgroup_v2.secgroup_eth1: Expected HTTP response code [201 202] when accessing [POST http://172.16.1.20:9696/v2.0/security-groups], but got 409 instead{\\\"NeutronError\\\": {\\\"message\\\": \\\"Quota exceeded for resources: ['security_group'].\\\", \\\"type\\\": \\\"OverQuota\\\", \\\"detail\\\": \\\"\\\"}}\"}",
"status": "FAILED",
"error": "{\"msg\":\"[]1 error(s) occurred:* openstack_networking_secgroup_v2.secgroup_eth1: 1 error(s) occurred:* openstack_networking_secgroup_v2.secgroup_eth1: Expected HTTP response code [201 202] when accessing [POST http://172.16.1.20:9696/v2.0/security-groups], but got 409 instead{\\\"NeutronError\\\": {\\\"message\\\": \\\"Quota exceeded for resources: ['security_group'].\\\", \\\"type\\\": \\\"OverQuota\\\", \\\"detail\\\": \\\"\\\"}}\"}"
},
"serviceId": "iot-yf-TNemt3uqQf"
}
},
"relyMethodReturnMocks": {
"productInsts": [
{
"id": "4028121b724a842701724fbb78000fc2",
"name": "云堡垒机-安全组测试",
"description": ""
}
],
"exceptions": [
{
"name": "java.io.IOException",
"message": ""
}
]
},
"exceptSuccessResults": {
},
"exceptFailureResults": {
"exceptions": [
{
"name": "java.io.IOException",
"message": ""
}
]
}
}
]
}
示例
package com.inspur.securityhubapi.mq;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.test.util.ReflectionTestUtils;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
...
public class CsfMessageListenerTest extends BaseTest {
@InjectMocks
private CsfMessageListener csfMessageListener;
...
@Mock
private BssMessageSender bssMessageSender;
private List<JSONObject> responseMessage;
private List<ProductInst> productInstList;
private List<ProductInstOperation> productInstOperationList;
private List<ProductInstProp> productInstPropList;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
...
String productInstPropProp = "/mock/json/product_inst_prop.json";
productInstPropList = JsonUtils.readObjects(productInstPropProp, ProductInstProp.class);
ReflectionTestUtils.setField(csfMessageListener, "sendMockMessageOn", true);
}
@Test
public void createSecurityGroup() throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
String path = "/mock/json/mq/CsfMessageListenerTest.json";
TestCaseUtils testCaseUtils = new TestCaseUtils("createSecurityGroup", path);
List<TestCase> testCases = testCaseUtils.getTestCases();
for (TestCase testCase : testCases) {
// 获取测试用例描述
String description = testCase.getDescription();
System.out.println("测试用例:" + description);
// 获取测试用例入参
JSONObject messageBody = testCase.getArg("messageBody", JSONObject.class);
Message message = new Message(messageBody.toJSONString().getBytes(), new MessageProperties());
// 获取测试用例对其他依赖方法的返回体
when(objectMapper.readValue(any(byte[].class), any(Class.class))).thenReturn(messageBody);
ProductInst productInst = testCase.getRelyMethodReturnMock("productInsts", 0, ProductInst.class);
when(productInstService.getProductInstByServiceId(anyString())).thenReturn(productInst);
when(productInstService.updateProductInstByServiceId(anyString(), any(ProductInst.class))).thenReturn(productInst);
doNothing().when(productInstService).deleteProductInst(anyString());
when(productInstOperationService.saveProductInstOperation(anyString(), anyString(), anyBoolean(), anyString(), anyString())).thenReturn(null);
when(websocketService.sendWebsocketInfo(any(ProductInst.class), anyString(), anyString())).thenReturn(Boolean.TRUE);
doNothing().when(bssMessageSender).senderCreateFailMessage(any(ProductInst.class));
if (testCase.hasExceptSuccessResults()) {
// 获取期待结果
Map<String, Integer> exceptMethodCallCounts = testCase.getExceptSuccessResult("exceptMethodCallCounts", 0, Map.class);
// 执行待测试的方法逻辑
csfMessageListener.createSecurityGroup(message, null);
// 断言
verify(bssMessageSender, times(exceptMethodCallCounts.get("bssMessageSender.senderCreateFailMessage"))).senderCreateFailMessage(any(ProductInst.class));
System.out.println();
}
if (testCase.hasExceptFailureResults()) {
// 获取测试用例对依赖方法的返回体
JSONObject exceptionJson = testCase.getRelyMethodReturnMock("exceptions", 0, JSONObject.class);
Exception exceptionMock = (Exception) Class.forName(exceptionJson.getString("name"))
.getConstructor(String.class).newInstance(exceptionJson.getString("message"));
when(objectMapper.readValue(any(byte[].class), any(Class.class))).thenThrow(exceptionMock);
try {
// 执行待测试的方法逻辑
csfMessageListener.createSecurityGroup(message, null);
fail();
} catch (Exception e) {
// 获取期待的异常
JSONObject expectedException = testCase.getExceptFailureResult("exceptions", 0, JSONObject.class);
Assert.assertEquals(expectedException.getString("name"), e.getClass().getName());
}
}
}
}
...
}
Last updated
Was this helpful?