Java JSON 序列化


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 单元测试用例时最大的妙用有两点:

  1. JSON 反序列化字符串为数据对象,大大减少了数据对象的模拟代码;

  2. 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 个选择:

  1. 放在“src/test/java”目录下,跟测试类放在同一目录下;
  2. 放在“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 资源文件,但也可以是其它类型的资源文件。

综合示例如下:

image

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 资源数据。

我们也可以用集成测试得到方法内部的方法调用的参数值和返回值,具体方法如下:

  1. 首先,在源代码中添加日志输出语句;

  2. 然后,执行单元测试用例,得到对应的方法调用参数值和返回值;

  3. 最后,删除源代码中日志输出语句,恢复源代码为原来的样子。

来源于测试过程

有一些数据,是由被测方法生成的,比如:方法返回值和调用参数。针对这类数据,可以在测试过程中生成,然后逐一进行数据核对,最后整理成 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 初始化对象,这个问题在网上一直有争论,双方都各有自己的理由。

这里,进行了简单的归纳总结如下:

image




[参考文章](https://mp.weixin.qq.com/s/HncA8TI7vuqW1lmnhfdv7A "参考文章")
posted @ 2021-10-21 23:50  Juno3550  阅读(6547)  评论(0编辑  收藏  举报