mcpField
U
unit-test-json-serialization

unit-test-json-serialization

github.com/giuseppe-trisciuoglio测试与质量3653.8k 次安装7 周前更新
代码仓库

关于 unit-test-json-serialization

这项技能介绍如何用 Spring 的 `@JsonTest` 和 Jackson 测试 JSON 序列化与反序列化,适用于 Java 开发者。内容涵盖映射、自定义序列化器、嵌套数据、日期格式及边界情况。

主要功能

  1. 01测试序列化与反序列化
  2. 02用 JacksonTester 检查 JSON 路径
  3. 03覆盖自定义映射与序列化器
  4. 04测试嵌套对象和日期格式
  5. 05验证空值、缺失字段及多态类型

适用场景

  1. 01验证 DTO 的 JSON 输入与输出
  2. 02测试自定义 Jackson 序列化器和反序列化器
  3. 03检查日期格式和字段名映射
  4. 04发现空值与多态处理的回归问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-json-serialization

安装后在 Claude Code 中输入/unit-test-json-serialization确认已加载。

详细介绍

使用 @JsonTest 对 JSON 序列化进行单元测试

概述

介绍如何使用 Spring 的 @JsonTest 和 Jackson 对 JSON 序列化与反序列化进行单元测试。涵盖 POJO 映射、自定义序列化器、字段名称映射、嵌套对象、日期/时间格式化和多态类型。

适用场景

  • 测试 DTO 的 JSON 序列化/反序列化
  • 验证自定义 Jackson 序列化器/反序列化器
  • 验证 @JsonProperty、@JsonIgnore 和字段名称映射
  • 测试日期/时间格式处理(LocalDateTime、Date)
  • 测试 null 处理和缺失字段
  • 测试多态类型反序列化

说明

  1. 为测试类添加 @JsonTest 注解 → 启用 JacksonTester 自动配置
  2. 为目标类型自动装配 JacksonTester → 提供类型安全的 JSON 断言
  3. 测试序列化 → 调用 json.write(object),并使用 extractingJsonPath* 断言 JSON 路径
  4. 测试反序列化 → 调用 json.parse(json) 或 json.parseObject(json),并断言对象状态
  5. 验证往返转换 → 序列化后再反序列化,验证数据是否一致(前提是对象支持正确的比较)
  6. 测试边界情况 → null 值、缺失字段、空集合、无效 JSON
  7. 添加验证检查点:每次断言后,验证数据错误时测试能否给出有意义的失败结果

示例

Maven 配置
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
Gradle 配置
dependencies {
  implementation("org.springframework.boot:spring-boot-starter-json")
  testImplementation("org.springframework.boot:spring-boot-starter-test")
}
基本序列化和反序列化
@JsonTest
class UserDtoJsonTest {

  @Autowired
  private JacksonTester<UserDto> json;

  @Test
  void shouldSerializeUserToJson() throws Exception {
    UserDto user = new UserDto(1L, "Alice", "[email protected]", 25);
    JsonContent<UserDto> result = json.write(user);

    result
      .extractingJsonPathNumberValue("$.id").isEqualTo(1)
      .extractingJsonPathStringValue("$.name").isEqualTo("Alice")
      .extractingJsonPathStringValue("$.email").isEqualTo("[email protected]")
      .extractingJsonPathNumberValue("$.age").isEqualTo(25);
  }

  @Test
  void shouldDeserializeJsonToUser() throws Exception {
    String json_content = "{\"id\":1,\"name\":\"Alice\",\"email\":\"[email protected]\",\"age\":25}";
    UserDto user = json.parse(json_content).getObject();

    assertThat(user.getId()).isEqualTo(1L);
    assertThat(user.getName()).isEqualTo("Alice");
    assertThat(user.getEmail()).isEqualTo("[email protected]");
    assertThat(user.getAge()).isEqualTo(25);
  }

  @Test
  void shouldHandleNullFields() throws Exception {
    String json_content = "{\"id\":1,\"name\":null,\"email\":\"[email protected]\"}";
    UserDto user = json.parse(json_content).getObject();
    assertThat(user.getName()).isNull();
  }
}
自定义 JSON 属性
public class Order {
  @JsonProperty("order_id")
  private Long id;

  @JsonProperty("total_amount")
  private BigDecimal amount;

  @JsonIgnore
  private String internalNote;
}

@JsonTest
class OrderJsonTest {

  @Autowired
  private JacksonTester<Order> json;

  @Test
  void shouldMapJsonPropertyNames() throws Exception {
    String json_content = "{\"order_id\":123,\"total_amount\":99.99}";
    Order order = json.parse(json_content).getObject();
    assertThat(order.getId()).isEqualTo(123L);
    assertThat(order.getAmount()).isEqualByComparingTo(new BigDecimal("99.99"));
  }

  @Test
  void shouldIgnoreJsonIgnoreFields() throws Exception {
    Order order = new Order(123L, new BigDecimal("99.99"));
    order.setInternalNote("Secret");
    assertThat(json.write(order).json).doesNotContain("internalNote");
  }
}
嵌套对象
public class Product {
  private Long id;
  private String name;
  private Category category;
  private List<Review> reviews;
}

@JsonTest
class ProductJsonTest {

  @Autowired
  private JacksonTester<Product> json;

  @Test
  void shouldSerializeNestedObjects() throws Exception {
    Product product = new Product(1L, "Laptop", new Category(1L, "Electronics"));
    JsonContent<Product> result = json.write(product);

    result
      .extractingJsonPathNumberValue("$.category.id").isEqualTo(1)
      .extractingJsonPathStringValue("$.category.name").isEqualTo("Electronics");
  }

  @Test
  void shouldDeserializeNestedObjects() throws Exception {
    String json_content = "{\"id\":1,\"name\":\"Laptop\",\"category\":{\"id\":1,\"name\":\"Electronics\"}}";
    Product product = json.parse(json_content).getObject();
    assertThat(product.getCategory().getName()).isEqualTo("Electronics");
  }

  @Test
  void shouldHandleListOfNestedObjects() throws Exception {
    String json_content = "{\"id\":1,\"reviews\":[{\"rating\":5},{\"rating\":4}]}";
    Product product = json.parse(json_content).getObject();
    assertThat(product.getReviews()).hasSize(2);
  }
}
日期/时间格式化
@JsonTest
class DateTimeJsonTest {

  @Autowired
  private JacksonTester<Event> json;

  @Test
  void shouldFormatDateTimeCorrectly() throws Exception {
    LocalDateTime dt = LocalDateTime.of(2024, 1, 15, 10, 30, 0);
    json.write(new Event("Conference", dt))
      .extractingJsonPathStringValue("$.scheduledAt").isEqualTo("2024-01-15T10:30:00");
  }
}
自定义序列化器
public class CustomMoneySerializer extends JsonSerializer<BigDecimal> {
  @Override
  public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
    gen.writeString(value == null ? null : String.format("$%.2f", value));
  }
}

@JsonTest
class CustomSerializerTest {

  @Autowired
  private JacksonTester<Price> json;

  @Test
  void shouldUseCustomSerializer() throws Exception {
    json.write(new Price(new BigDecimal("99.99")))
      .extractingJsonPathStringValue("$.amount").isEqualTo("$99.99");
  }
}
多态反序列化
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
  @JsonSubTypes.Type(value = CreditCard.class, name = "credit_card"),
  @JsonSubTypes.Type(value = PayPal.class, name = "paypal")
})
public abstract class PaymentMethod { }

@JsonTest
class PolymorphicJsonTest {

  @Autowired
  private JacksonTester<PaymentMethod> json;

  @Test
  void shouldDeserializeCreditCard() throws Exception {
    String json_content = "{\"type\":\"credit_card\",\"id\":\"card123\"}";
    assertThat(json.parse(json_content).getObject()).isInstanceOf(CreditCard.class);
  }

  @Test
  void shouldDeserializePayPal() throws Exception {
    String json_content = "{\"type\":\"paypal\",\"id\":\"pp123\"}";
    assertThat(json.parse(json_content).getObject()).isInstanceOf(PayPal.class);
  }
}

最佳实践

  • 同时测试序列化和反序列化,以实现完整覆盖
  • 单独验证 JSON 路径,而不是比较完整的 JSON 字符串
  • 明确测试 null 处理——根据 @JsonInclude,null 字段可能会被包含或排除
  • 使用 extractingJsonPath* 方法精确断言字段
  • 测试往返转换:序列化对象,再反序列化 JSON,并验证结果是否一致
  • 验证边界情况:空字符串、空集合、深层嵌套结构
  • 在单个测试中组合相关断言,以提高清晰度

限制和警告

  • @JsonTest 加载的上下文有限:仅加载与 JSON 相关的 Bean;需要完整 Spring 上下文时,请使用 @SpringBootTest
  • Jackson 版本:确保注解版本与所用的 Jackson 版本一致
  • 日期格式:默认使用 ISO-8601;自定义格式请使用 @JsonFormat
  • null 处理:使用 @JsonInclude(Include.NON_NULL) 在序列化时排除 null 值
  • 循环引用:使用 @JsonManagedReference/@JsonBackReference 防止无限循环
  • 不可变对象:使用 @JsonCreator + @JsonProperty 进行基于构造函数的反序列化
  • 多态类型:@JsonTypeInfo 必须能够正确识别子类型,反序列化才能正常工作

调试工作流

当 JSON 测试失败时,请遵循以下工作流:

失败症状常见原因验证方法
JsonPath 断言失败字段名称不匹配检查 @JsonProperty 的拼写是否与 JSON 键匹配
预期为 null,但得到了值配置了 @JsonInclude(NON_NULL)检查字段或类上的注解
反序列化返回了错误的类型缺少 @JsonTypeInfo在 JSON 中添加类型信息属性,或配置子类型映射
日期格式不匹配格式字符串不正确确认 @JsonFormat(pattern=...) 与预期字符串匹配
输出中缺少字段使用了 @JsonIgnore 或 transient 修饰符检查字段是否带有 @JsonIgnore 或 transient 关键字
嵌套对象为 null内层 JSON 缺失或格式错误记录解析后的 JSON;检查内层结构是否与 POJO 匹配
JsonParseExceptionJSON 字符串格式错误验证 JSON 语法;检查是否存在未转义字符

修复后的验证检查点:重新运行测试;如果通过,请为相反情况编写一个配套测试(例如,如果修复了 null 处理问题,则添加一个针对非 null 值的测试,以防止回归)。

参考资料

评论

支持 Markdown