JSON 序列化 API
序列化指把對象通過流的方式存儲到文件中,反序列化則是指把文件中的字節內容讀出來並還原成 Java 對象。
JSON 序列化是快速編寫 Java 單元測試用例的技巧之一。這里以 Fastjson 為例,介紹一些 JSON 序列化技巧。
Fastjson 簡介
Fastjson 是一個 Java 庫,可以將 Java 對象轉換為 JSON 格式,當然它也可以將 JSON 字符串轉換為 Java 對象。
Fastjson 可以操作任何 Java 對象,即使是一些預先存在的沒有源碼的對象。
Fastjson 源碼地址
Fastjson 中文 Wiki
Fastjson 特性:
- 提供服務器端、安卓客戶端兩種解析工具,性能表現較好。
- 提供了 toJSONString() 和 parseObject() 方法來將 Java 對象與 JSON 相互轉換。調用toJSONString 方法即可將對象轉換成 JSON 字符串,parseObject 方法則反過來將 JSON 字符串轉換成對象。
- 允許轉換預先存在的無法修改的對象(比如只有 class 但無源代碼的對象)。
- Java 泛型的廣泛支持。
- 允許對象的自定義表示、允許自定義序列化類。
- 支持任意復雜對象(具有深厚的繼承層次和廣泛使用的泛型類型)。
下載和使用:
你可以在 maven 中央倉庫中直接下載,或者配置 Maven 依賴:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>x.x.x</version> <!-- 根據需要使用特定版本,建議使用最新版本 -->
</dependency>
序列化:toJSONString()
序列化對象
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONField;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class User {
/**
* @JSONField 作用:自定義對象屬性所對應的 JSON 鍵名
* @JSONField 的作用對象:
* 1. Field
* 2. Setter 和 Getter 方法
* 注意:
* 1. 若屬性是私有的,必須要有 set 方法,否則反序列化會失敗。
* 2. 若沒有 @JSONField 注解,則直接使用屬性名。
*/
@JSONField(name="NAME")
private String name;
@JSONField(name="AGE")
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class ObjectTest {
private static List<User> userList = new ArrayList<User>();
@BeforeAll
public static void setUp() {
userList.add(new User("xiaoming", 18));
userList.add(new User("xiaodan", 19));
}
@DisplayName("序列化對象")
@Test
public void testObjectToJson() {
String userJson = JSON.toJSONString(userList.get(0));
System.out.println(userJson); // {"AGE":18,"NAME":"xiaoming"}
}
@DisplayName("序列化集合")
@Test
public void testListToJson() {
String userListJson = JSON.toJSONString(userList);
System.out.println(userListJson); // [{"AGE":18,"NAME":"xiaoming"},{"AGE":19,"NAME":"xiaodan"}]
}
@DisplayName("序列化數組")
@Test
public void testArrayToJson() {
User[] userArray = new User[5];
userArray[0] = new User("zhangsan", 20);
userArray[1] = new User("lisi", 21);
String userArrayJson = JSON.toJSONString(userArray);
System.out.println(userArrayJson); // [{"AGE":20,"NAME":"zhangsan"},{"AGE":21,"NAME":"lisi"},null,null,null]
}
@DisplayName("序列化映射")
@Test
public void testMapToJson() {
Map<Integer, User> userMap = new HashMap<Integer, User>();
userMap.put(1, new User("xiaotie", 10));
userMap.put(2, new User("xiaoliu", 11));
String userMapJson = JSON.toJSONString(userMap);
System.out.println(userMapJson); // {1:{"AGE":10,"NAME":"xiaotie"},2:{"AGE":11,"NAME":"xiaoliu"}}
}
}
序列化指定屬性字段
利用 JSON.toJSONString 方法序列化指定屬性字段,主要通過設置屬性預過濾器(SimplePropertyPreFilter)的包含屬性字段列表(includes)實現。
主要應用於只想驗證某些字段的情況,比如只驗證跟測試用例有關的字段。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.serializer.SimplePropertyPreFilter;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.*;
class User {
/**
* @JSONField 作用:自定義對象屬性所對應的 JSON 鍵名
* @JSONField 的作用對象:
* 1. Field
* 2. Setter 和 Getter 方法
* 注意:
* 1. 若屬性是私有的,必須要有 set 方法,否則反序列化會失敗。
* 2. 若沒有 @JSONField 注解,則直接使用屬性名。
*/
@JSONField(name="NAME")
private String name;
@JSONField(name="AGE")
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class ObjectTest {
@DisplayName("指定所有類的屬性字段")
@Test
public void testAllClassField() {
User user = new User("xiaoming", 18);
SimplePropertyPreFilter filter = new SimplePropertyPreFilter(); // 默認所有類型的類均可轉換
filter.getIncludes().addAll(Arrays.asList("NAME", "AGE")); // 需存在於 @JSONField
String text = JSON.toJSONString(user, filter);
System.out.println(text); // {"AGE":18,"NAME":"xiaoming"}
}
@DisplayName("指定單個類的個別屬性字段")
@Test
public void testOneClassField() {
ArrayList<User> users = new ArrayList<>();
users.add(new User("xiaodan", 18));
users.add(new User("xiaoxue", 19));
SimplePropertyPreFilter filter = new SimplePropertyPreFilter(User.class); // 指定User類
filter.getIncludes().addAll(Arrays.asList("NAME"));
String text = JSON.toJSONString(users, filter);
System.out.println(text); // [{"NAME":"xiaodan"},{"NAME":"xiaoxue"}]
}
}
序列化排除屬性字段
利用 JSON.toJSONString 方法序列化過濾屬性字段,主要通過設置屬性預過濾器(SimplePropertyPreFilter)的排除屬性字段列表(excludes)實現。
主要應用於不想驗證某些字段的情況,比如排除無法驗證的隨機屬性字段。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.serializer.SimplePropertyPreFilter;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.*;
class User {
/**
* @JSONField 作用:自定義對象屬性所對應的 JSON 鍵名
* @JSONField 的作用對象:
* 1. Field
* 2. Setter 和 Getter 方法
* 注意:
* 1. 若屬性是私有的,必須要有 set 方法,否則反序列化會失敗。
* 2. 若沒有 @JSONField 注解,則直接使用屬性名。
*/
@JSONField(name="NAME")
private String name;
@JSONField(name="AGE")
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class ObjectTest {
@DisplayName("排除所有類的屬性字段")
@Test
public void testAllClassField() {
User user = new User("xiaoming", 18);
SimplePropertyPreFilter filter = new SimplePropertyPreFilter(); // 默認所有類型的類均可轉換
filter.getExcludes().addAll(Arrays.asList("NAME")); // 排除 NAME 字段(需存在於 @JSONField)
String text = JSON.toJSONString(user, filter);
System.out.println(text); // {"AGE":18}
}
@DisplayName("排除指定類的屬性字段")
@Test
public void testOneClassField() {
ArrayList<User> users = new ArrayList<>();
users.add(new User("xiaodan", 18));
users.add(new User("xiaoxue", 19));
SimplePropertyPreFilter filter = new SimplePropertyPreFilter(User.class); // 指定User類
filter.getExcludes().addAll(Arrays.asList("AGE"));
String text = JSON.toJSONString(users, filter);
System.out.println(text); // [{"AGE":18},{"AGE":19}]
}
}
反序列化:parseObject() / parseArray()
反序列化對象
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.alibaba.fastjson.annotation.JSONField;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.*;
class User {
/**
* @JSONField 作用:自定義對象屬性所對應的 JSON 鍵名
* @JSONField 的作用對象:
* 1. Field
* 2. Setter 和 Getter 方法
* 注意:
* 1. 若屬性是私有的,必須要有 set 方法,否則反序列化會失敗。
* 2. 若沒有 @JSONField 注解,則直接使用屬性名。
*/
// @JSONField(name="name")
private String name;
// @JSONField(name="age")
private int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class ObjectTest {
@DisplayName("反序列化對象")
@Test
public void testJsonToObject() {
String text = "{\"age\":18,\"name\":\"xiaoming\"}";
User user = JSON.parseObject(text, User.class);
System.out.println(user); // User{name='xiaoming', age=18}
}
@DisplayName("反序列化數組")
@Test
public void testJsonToArray() {
String text = "[{\"age\":18,\"name\":\"xiaoming\"}, {\"age\":19,\"name\":\"xiaowa\"}]";
User[] users = JSON.parseObject(text, User[].class);
System.out.println(Arrays.toString(users)); // [User{name='xiaoming', age=18}, User{name='xiaowa', age=19}]
}
@DisplayName("反序列化集合")
@Test
public void testJsonToCollection() {
String text = "[{\"age\":18,\"name\":\"xiaoming\"}, {\"age\":19,\"name\":\"xiaowa\"}]";
// List 集合
List<User> userList = JSON.parseArray(text, User.class);
System.out.println(Arrays.toString(userList.toArray())); // [User{name='xiaoming', age=18}, User{name='xiaowa', age=19}]
// Set 集合
Set<User> userSet = JSON.parseObject(text, new TypeReference<Set<User>>() {});
System.out.println(Arrays.toString(userSet.toArray())); // [User{name='xiaowa', age=19}, User{name='xiaoming', age=18}]
}
@DisplayName("反序列化映射")
@Test
public void testJsonToMap() {
String text = "{1:{\"age\":18,\"name\":\"xiaoming\"}, 2:{\"age\":19,\"name\":\"xiaowa\"}}";
Map<Integer, User> userList = JSON.parseObject(text, new TypeReference<Map<Integer, User>>() {});
for (Integer i : userList.keySet()) {
System.out.println(userList.get(i));
}
/*
User{name='xiaoming', age=18}
User{name='xiaowa', age=19}
*/
}
}
反序列化非公有字段
由於某些屬性字段沒有公有設置方法,或者沒有以字段名稱作為公有設置方法,那么當需要反序列化這些屬性字段時,需要指定 SupportNonPublicField(支持非公有字段)反序列化參數。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.*;
class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private String getName() {
return name;
}
private void setName(String name) {
this.name = name;
}
private int getAge() {
return age;
}
private void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class PrivateTest {
@DisplayName("反序列化非公有字段")
@Test
public void testJsonToObject() {
String text = "{\"age\":18,\"name\":\"xiaoming\"}";
Person person = JSON.parseObject(text, Person.class, Feature.SupportNonPublicField);
System.out.println(person.toString()); // Person{name='xiaoming', age=18}
}
}
簡化冗長的單元測試代碼
JSON 序列化在編寫 Java 單元測試用例時最大的妙用有兩點:
-
JSON 反序列化字符串為數據對象,大大減少了數據對象的模擬代碼;
-
JSON 序列化數據對象為字符串,把數據對象驗證簡化為字符串驗證,大大減少了數據對象的驗證代碼。
簡化數據模擬代碼
非序列化方式的冗長代碼:
/**
* 模擬類屬性值
*/
Map<Long, String> languageMap = new HashMap<>(MapHelper.DEFAULT);
languageMap.put(1L, "Java");
languageMap.put(2L, "C++");
languageMap.put(3L, "Python");
languageMap.put(4L, "JavaScript");
... // 約幾十行
Whitebox.setInternalState(developmentService, "languageMap", languageMap);
/**
* 模擬方法參數值
*/
List<UserCreateVO> userCreateList = new ArrayList<>();
UserCreateVO userCreate0 = new UserCreateVO();
userCreate0.setName("Changyi");
userCreate0.setTitle("Java Developer");
... // 約幾十行
userCreateList.add(userCreate0);
UserCreateVO userCreate1 = new UserCreateVO();
userCreate1.setName("Tester");
userCreate1.setTitle("Java Tester");
... // 約幾十行
userCreateList.add(userCreate1);
... // 約幾十條
userService.batchCreate(userCreateList);
/**
* 模擬方法返回值
*/
Long companyId = 1L;
List<UserDO> userList = new ArrayList<>();
UserDO user0 = new UserDO();
user0.setId(1L);
user0.setName("Changyi");
user0.setTitle("Java Developer");
... // 約幾十行
userList.add(user0);
UserDO user1 = new UserDO();
user1.setId(2L);
user1.setName("Tester");
user1.setTitle("Java Tester");
... // 約幾十行
userList.add(user1);
... // 約幾十條
Mockito.doReturn(userList).when(userDAO).queryByCompanyId(companyId);
采用序列化簡化:
對於數據模擬,首先需要先加載 JSON 資源文件為字符串,然后通過 JSON 反序列化字符串為數據對象,最后用於模擬類屬性值、方法參數值和方法返回值。這樣,就精簡了原來冗長的賦值語句。
/**
* 模擬類屬性值
* languageMap.json 文件內容:{1:"Java",2:"C++",3:"Python",4:"JavaScript"...}
*/
String text = ResourceHelper.getResourceAsString(getClass(), path + "languageMap.json");
Map<Long, String> languageMap = JSON.parseObject(text, new TypeReference<Map<Long, String>>() {});
Whitebox.setInternalState(mobilePhoneService, "languageMap", languageMap);
/**
* 模擬方法參數值
* userCreateList.json 文件內容:[{"name":"Changyi","title":"Java Developer"...},{"name":"Tester","title":"Java Tester"...},...]
*/
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateList.json");
List<UserCreateVO> userCreateList = JSON.parseArray(text, UserCreateVO.class);
userService.batchCreate(userCreateList);
/**
* 模擬方法返回值
* userList.json 文件內容:[{"id":1,"name":"Changyi","title":"Java Developer"...},{"id":2,"name":"Tester","title":"Java Tester"...},...]
*/
Long companyId = 1L;
String text = ResourceHelper.getResourceAsString(getClass(), path + "userList.json");
List<UserDO> userList = JSON.parseArray(text, UserDO.class);
Mockito.doReturn(userList).when(userDAO).queryByCompanyId(companyId);
簡化數據驗證代碼
非序列方式的冗長代碼:
/**
* 驗證方法返回值
*/
Long companyId = 1L;
List<UserVO> userList = userService.queryByCompanyId(companyId);
UserVO user0 = userList.get(0);
Assert.assertEquals("name不一致", "Changyi", user0.getName());
Assert.assertEquals("title不一致", "Java Developer", user0.getTitle());
... // 約幾十行
UserVO user1 = userList.get(1);
Assert.assertEquals("name不一致", "Tester", user1.getName());
Assert.assertEquals("title不一致", "Java Tester", user1.getTitle());
... // 約幾十行
... // 約幾十條
/**
* 驗證方法參數值
*/
ArgumentCaptor<List<UserDO>> userCreateListCaptor = CastUtils.cast(ArgumentCaptor.forClass(List.class));
Mockito.verify(userDAO).batchCreate(userCreateListCaptor.capture());
List<UserDO> userCreateList = userCreateListCaptor.getValue();
UserDO userCreate0 = userCreateList.get(0);
Assert.assertEquals("name不一致", "Changyi", userCreate0.getName());
Assert.assertEquals("title不一致", "Java Developer", userCreate0.getTitle());
... // 約幾十行
UserDO userCreate1 = userCreateList.get(1);
Assert.assertEquals("name不一致", "Tester", userCreate1.getName());
Assert.assertEquals("title不一致", "Java Tester", userCreate1.getTitle());
... // 約幾十行
... // 約幾十條
采用序列化方式簡化:
對於數據驗證,首先需要先加載 JSON 資源文件為字符串,然后通過 JSON 序列化數據對象為字符串,最后驗證兩字符串是否一致。這樣,就精簡了原來冗長的驗證語句。
/**
* 驗證方法返回值
* userList.json 文件內容:[{"name":"Changyi","title":"Java Developer"...},{"name":"Tester","title":"Java Tester"...},...]
*/
Long companyId = 1L;
List<UserVO> userList = userService.queryByCompanyId(companyId);
String text = ResourceHelper.getResourceAsString(getClass(), path + "userList.json");
Assert.assertEquals("用戶列表不一致", text, JSON.toJSONString(userList));
/**
* 驗證方法參數值
* userCreateList.json 文件內容:[{"name":"Changyi","title":"Java Developer"...},{"name":"Tester","title":"Java Tester"...},...]
*/
ArgumentCaptor<List<UserDO>> userCreateListCaptor = CastUtils.cast(ArgumentCaptor.forClass(List.class));
Mockito.verify(userDAO).batchCreate(userCreateListCaptor.capture());
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateList.json");
Assert.assertEquals("用戶創建列表不一致", text, JSON.toJSONString(userCreateListCaptor.getValue()));
測試用例及資源命名
為了更好地利用JSON序列化技巧,首先對測試用例和資源文件進行規范化命名。
測試類命名
按照行業慣例,測試類的命名應以被測試類名開頭並以 Test 結尾。
比如:UserService(用戶服務類)的測試類需要命名為 UserServiceTest(用戶服務測試類)。
單元測試類應該放在被測試類的同一工程的 "src/test/java" 目錄下,並且要放在被測試類的同一包下。
注意,單元測試類不允許寫在業務代碼目錄下,否則在編譯時沒法過濾這些測試用例。
測試方法命名
按照行業規范,測試方法命名應以 test 開頭並以被測試方法結尾。
比如:batchCreate(批量創建)的測試方法需要命名為 testBatchCreate(測試:批量創建),queryByCompanyId(根據公司標識查詢)的測試方法需要命名為 testQueryByCompanyId(測試:根據公司標識查詢)。
當一個方法對應多個測試用例時,就需要創建多個測試方法,原有測試方法命名已經不能滿足需求了。
有人建議在原有的測試方法命名的基礎上,添加 123 等序號表示不同的用例。比如:testBatchCreate1(測試:批量創建1)、testBatchCreate2(測試:批量創建2)……但是,這種方法不能明確每個單元測試的用意。
這里,建議在原有的測試方法命名的基礎上,添加“With+條件”來表達不同的測試用例方法。
按照結果命名:
- testBatchCreateWithSuccess(測試:批量創建-成功);
- testBatchCreateWithFailure(測試:批量創建-失敗);
- testBatchCreateWithException(測試:批量創建-異常);
按照參數命名:
- testBatchCreateWithListNull(測試:批量創建-列表為NULL);
- testBatchCreateWithListEmpty(測試:批量創建-列表為空);
- testBatchCreateWithListNotEmpty(測試:批量創建-列表不為空);
按照意圖命名:
- testBatchCreateWithNormal(測試:批量創建-正常);
- testBatchCreateWithGray(測試:批量創建-灰度);
- testBatchCreateWithException(測試:批量創建-異常);
當然,還有形成其它的測試方法命名方式,也可以把不同的測試方法命名方式混用,只要能清楚地表達出這個測試用例的涵義即可。
測試類資源目錄命名
這里,建議的資源目錄命名方式為以 test 開頭且以被測試類名結尾。
比如:UserService(用戶服務類)的測試資源目錄可以命名為 testUserService。
那么,這個資源目錄應該放在哪兒了?這里建議 2 個選擇:
- 放在“src/test/java”目錄下,跟測試類放在同一目錄下;
- 放在“src/test/resources”目錄下,跟測試類放在同一目錄下(建議 IDEA 用戶采用這種方式)。
測試方法資源目錄命名
在前面的小節中,我們針對測試方法進行了規范命名。這里,我們可以直接拿來使用,即用測試方法名稱來命名測試目錄。當然,這些測試方法資源目錄應該放在測試類資源目錄下。
比如:測試類 UserServiceTest(用戶服務測試類)的測試方法testBatchCreateWithSuccess(測試:批量創建-成功)的測試資源目錄就是 testUserService/testBatchCreateWithSuccess。
另外,也可以采用“測試方法名稱”+“測試條件名稱”二級目錄的命名方式。
比如:測試類 UserServiceTest(用戶服務測試類)的測試方法testBatchCreateWithSuccess(測試:批量創建-成功)的測試資源目錄就是 testUserService/testBatchCreate/success。
這里,首推的是第一種方式,因為測試方法名稱和資源目錄名稱能夠保持一致。
測試資源文件命名
在被測試代碼中,所有參數、變量都已經有了命名。所以,建議優先使用這些參數和變量的名稱,並加后綴“.json”標識文件格式。如果這些資源文件名稱沖突,可以添加前綴以示區分。
比如:“userCreateList”的資源文件名稱為“userCreateList.json”。
另外,在測試用例代碼中,把這些測試資源文件加載后,反序列化為對應的數據對象,這些數據對象的變量名稱也應該跟資源文件名稱保持一致。
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateList.json");
List<UserCreateVO> userCreateList = JSON.parseArray(text, UserCreateVO.class);
userService.batchCreate(userCreateList);
測試資源文件存儲
在測試資源目錄和名稱定義好之后,就需要存入測試資源文件了。存儲方式總結如下:
-
如果是測試類下所有測試用例共用的資源文件,建議存儲在測試類資源目錄下,比如:testUserService;
-
如果是測試用例獨有的資源文件,建議存儲在測試方法資源目錄下,比如:testUserService/testBatchCreateWithSuccess;
-
如果是某一被測方法所有的測試用例共用的資源文件,建議存儲在不帶任何修飾的測試方法資源目錄下,比如:testUserService/testBatchCreate;
-
如果測試類資源目錄下只有一個測試方法資源目錄,可以去掉這個測試方法資源目錄,把所有資源文件存儲在測試類資源目錄下。
注意:這里的資源文件不光是 JSON 資源文件,但也可以是其它類型的資源文件。
綜合示例如下:
JSON 資源文件格式
關於 JSON 資源文件是否格式化的建議:不要格式化 JSON 資源文件內容,否則會占用更多的代碼行數,還會導致無法直接進行文本比較。
POM 文件配置
根項目的 pom.xml 文件需要做以下配置:
-
在屬性配置中,配置了單元測試所依賴的包版本;
-
在依賴配置中,配置了單元測試所依賴的包名稱;
-
在構建配置中,配置了編譯時需要拷貝目錄下的資源文件(如果有其它的資源文件格式,需要在 pom 中配置添加)。
<?xml version="1.0" encoding="UTF-8" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
...
<!-- 屬性管理 -->
<properties>
...
<junit.version>4.13.1</junit.version>
<mockito.version>3.3.3</mockito.version>
<powermock.version>2.0.9</powermock.version>
</properties>
<!-- 依賴管理 -->
<dependencyManagement>
<dependencies>
...
<!-- PowerMock -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 構建管理 -->
<build>
<pluginManagement>
<plugins>
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<executions>
...
<execution>
<id>copy-test-resources</id>
<phase>compile</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<encoding>UTF-8</encoding>
<outputDirectory>${project.build.directory}/test-classes</outputDirectory>
<resources>
<resource>
<directory>src/test/java</directory>
<includes>
<include>**/*.txt</include>
<include>**/*.csv</include>
<include>**/*.json</include>
<include>**/*.properties</include>
</includes>
</resource>
<resource>
<directory>src/test/resources</directory>
<includes>
<include>**/*.txt</include>
<include>**/*.csv</include>
<include>**/*.json</include>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
JSON 資源文件的來源
JSON 資源文件來源方式很多,以下幾種供大家參考。
來源於自己組裝
直接利用 JSON 編輯器或者純文本編輯器,自己一個字段一個字段地編寫 JSON 資源數據。
注意:這種方式容易出現 JSON 格式錯誤及字符串轉義問題。
來源於代碼生成
作為程序員,能夠用程序生成 JSON 資源數據,就絕不手工組裝 JSON 資源數據。下面,便是利用 Fastjson 的 JSON.toJSONString 方法生成 JSON 資源數據。
public static void main(String[] args) {
List<UserCreateVO> userCreateList = new ArrayList<>();
UserCreateVO userCreate0 = new UserCreateVO();
userCreate0.setName("Changyi");
userCreate0.setTitle("Java Developer");
... // 約幾十行
userCreateList.add(userCreate0);
UserCreateVO userCreate1 = new UserCreateVO();
userCreate1.setName("Tester");
userCreate1.setTitle("Java Tester");
... // 約幾十行
userCreateList.add(userCreate1);
... // 約幾十條
System.out.println(JSON.toJSONString(userCreateList));
}
執行該程序后,生成的 JSON 資源數據如下:
[{"name":"Changyi","title":"Java Developer"...},{"name":"Tester","title":"Java Tester"...},...]
注意:這種方式能夠避免 JSON 格式錯誤及字符串轉義問題。
來源於線上日志
如果是事后補充單元測試,首先想到的就是利用線上日志。比如:
2021-08-31 18:55:40,867 INFO [UserService.java:34] - 根據公司標識(1)查詢所有用戶:[{"id":1,"name":"Changyi","title":"Java Developer"...},{"id":2,"name":"Tester","title":"Java Tester"...},...]
從上面的日志中,我們可以得到方法 userDAO.queryByCompanyId 的請求參數 companyId 取值為“1”,返回結果為“[{"id":1,"name":"Changyi","title":"Java Developer"...},{"id":2,"name":"Tester","title":"Java Tester"...},...]”。
注意:要想得到現成的 JSON 資源數據,就必須輸出完整的 JSON 數據內容。但是,由於 JSON 數據內容過大,一般不建議全部輸出。所以,從線上日志中也不一定能夠拿到現成的 JSON 資源數據。
來源於集成測試
集成測試,就是把整個或部分項目環境運行起來,能夠連接數據庫、Redis、MetaQ、HSF等所依賴的第三方服務環境,然后測試某一個方法的功能是否能夠達到預期。
/**
* 用戶DAO測試類
*/
@Slf4j
@RunWith(PandoraBootRunner.class)
@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {ExampleApplication.class})
public class UserDaoTest {
/** 用戶DAO */
@Resource
private UserDAO userDAO;
/**
* 測試: 根據公司標識查詢
*/
@Test
public void testQueryByCompanyId() {
Long companyId = 1L;
List<UserDO> userList = userDAO.queryByCompanyId(companyId);
log.info("userList={}", JSON.toJSONString(userList));
}
}
執行上面集成測試用例,輸出的日志內容如下:
2021-08-31 18:55:40,867 INFO [UserDaoTest.java:24] - userList=[{"id":1,"name":"Changyi","title":"Java Developer"...},{"id":2,"name":"Tester","title":"Java Tester"...},...]
上面日志中,userList 后面的就是我們需要的 JSON 資源數據。
我們也可以用集成測試得到方法內部的方法調用的參數值和返回值,具體方法如下:
-
首先,在源代碼中添加日志輸出語句;
-
然后,執行單元測試用例,得到對應的方法調用參數值和返回值;
-
最后,刪除源代碼中日志輸出語句,恢復源代碼為原來的樣子。
來源於測試過程
有一些數據,是由被測方法生成的,比如:方法返回值和調用參數。針對這類數據,可以在測試過程中生成,然后逐一進行數據核對,最后整理成 JSON 資源文件。
被測方法:
public void batchCreate(List<UserCreate> createList) {
List<UserDO> userList = createList.stream()
.map(UserService::convertUser).collect(Collectors.toList());
userDAO.batchCreate(userList);
}
測試用例:
@Test
public void testBatchCreate() {
// 調用測試方法
List<UserCreate> createList = ...;
userService.batchCreate(createList);
// 驗證測試方法
ArgumentCaptor<List<UserDO>> userListCaptor = CastUtils.cast(ArgumentCaptor.forClass(List.class));
Mockito.verify(userDAO).batchCreate(userListCaptor.capture());
Assert.assertEquals("用戶列表不一致", "", JSON.toJSONString(userListCaptor.getValue()));
}
執行單元測試后,提示以下問題:
org.junit.ComparisonFailure: 用戶列表不一致 expected:<[]> but was:<[[{"name":"Changyi","title":"Java Developer"...},{"name":"Tester","title":"Java Tester"...},...]]>
上面的錯誤信息中,后面括號中的就是我們需要需要的 JSON 資源數據。
注意:一定要進行數據核對,這有可能是錯誤代碼生成的錯誤數據。用錯誤數據去驗證生成它的代碼,當然不會測試出其中的問題。
使用 new 還是 mock 初始化對象?
在上面的案例中,都采用 new 來初始化對象並采用 set 來模擬屬性值的。有些同學會問,為什么不采用 mock 來初始化對象、用 doReturn-when 來模擬屬性值?其實,都是一樣的效果,只是前者顯得更簡潔而已。
關於使用 new 還是 mock 初始化對象,這個問題在網上一直有爭論,雙方都各有自己的理由。
這里,進行了簡單的歸納總結如下:
[參考文章](https://mp.weixin.qq.com/s/HncA8TI7vuqW1lmnhfdv7A "參考文章")