> For the complete documentation index, see [llms.txt](https://moluo.gitbook.io/notes/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://moluo.gitbook.io/notes/bian-cheng-xue-xi/ce-shi/2.5-dan-yuan-ce-shi-shi-li.md).

# 2.5单元测试示例

单元测试常见问题

* 数据源不独立，常因为新增加的测试用例，影响到以前的测试用例
* 期待结果写在测试代码中，耦合度高
* 每次增加测试用例，都需要更改测试代码，测试代码不可复用

本小节中，我们将介绍一种优雅的测试用例方式

* 我们会使用一个json文件囊括测试用例所需的所有入参，依赖，期待结果，期待异常。从而没次增加测试用例，只需要向json文件中添加数据即可，无需改动代码
* 每一个方法拥有独立的数据源

## 实体类

### com.moluo.example.util.TestCase

```java
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

```java
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

```java
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

```javascript
{
  "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": ""
          }
        ]
      }
    }
  ]
}
```

## 示例

```java
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());
                }
            }
        }
    }
...
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://moluo.gitbook.io/notes/bian-cheng-xue-xi/ce-shi/2.5-dan-yuan-ce-shi-shi-li.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
