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?