JUnit 5 自定义注解:方法级 JSON 参数注入

JUnit 5 自定义注解:方法级 JSON 参数注入

为了实现 在测试方法上使用注解,并通过注解属性指定参数名称和 JSON 字符串(转换为 Java 对象),以下是基于 JUnit 5 正确扩展接口的解决方案:


一、实现步骤
1. 定义自定义注解

创建一个注解 @JsonTest,用于在测试方法上指定参数名和 JSON 字符串。

java 复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonTest {
    String parameterName(); // 方法参数的名称
    String jsonValue();     // JSON 字符串
}

2. 实现 BeforeEachCallback

在测试方法执行前解析注解,将 JSON 转换为对象并存储到上下文。

java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class JsonParameterPreprocessor implements BeforeEachCallback {
    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        Method testMethod = context.getRequiredTestMethod();
        if (testMethod.isAnnotationPresent(JsonTest.class)) {
            JsonTest annotation = testMethod.getAnnotation(JsonTest.class);
            String parameterName = annotation.parameterName();
            String jsonValue = annotation.jsonValue();

            // 遍历方法参数,匹配参数名
            for (Parameter param : testMethod.getParameters()) {
                if (param.getName().equals(parameterName)) { 
                    // 将 JSON 转换为对象
                    Object parameterValue = objectMapper.readValue(jsonValue, param.getType());
                    
                    // 存储到上下文
                    context.getStore(ExtensionContext.Namespace.GLOBAL)
                          .put(parameterName, parameterValue);
                    break;
                }
            }
        }
    }
}

3. 实现 ParameterResolver

从上下文中获取存储的对象并注入到测试参数。

java 复制代码
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;

public class JsonParameterResolver implements ParameterResolver {
    @Override
    public boolean supportsParameter(ParameterContext paramCtx, ExtensionContext extCtx) {
        String paramName = paramCtx.getParameter().getName();
        return extCtx.getStore(ExtensionContext.Namespace.GLOBAL).get(paramName) != null;
    }

    @Override
    public Object resolveParameter(ParameterContext paramCtx, ExtensionContext extCtx) {
        String paramName = paramCtx.getParameter().getName();
        return extCtx.getStore(ExtensionContext.Namespace.GLOBAL).get(paramName);
    }
}

4. 注册扩展

在测试类中注册自定义扩展。

java 复制代码
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith({JsonParameterPreprocessor.class, JsonParameterResolver.class})
public class JsonParameterTest {

    @JsonTest(parameterName = "person", jsonValue = "{\"name\":\"Alice\",\"age\":25}")
    @Test
    void testPerson(Person person) {
        System.out.println("Name: " + person.getName() + ", Age: " + person.getAge());
        Assertions.assertEquals("Alice", person.getName());
    }

    @JsonTest(parameterName = "address", jsonValue = "{\"city\":\"New York\",\"zipCode\":\"10001\"}")
    @Test
    void testAddress(Address address) {
        System.out.println("City: " + address.getCity() + ", Zip Code: " + address.getZipCode());
        Assertions.assertEquals("New York", address.getCity());
    }
}

二、数据模型定义

确保 JSON 结构与 Java 类匹配。

java 复制代码
public class Person {
    private String name;
    private int age;
    // Getter & Setter
}

public class Address {
    private String city;
    private String zipCode;
    // Getter & Setter
}

三、关键点说明
  1. 参数名匹配

    • 需要启用编译时保留参数名(Java 8+ 使用 -parameters 编译选项)。
    • 确保 @JsonTest(parameterName="xxx") 与测试方法参数名一致。
  2. JSON 转换

    • 使用 JacksonGson 反序列化 JSON。

    • 依赖示例(Maven):

      xml 复制代码
      <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          <version>2.13.3</version>
      </dependency>
  3. 上下文存储

    • 使用 ExtensionContext.Store 临时存储参数值,避免线程安全问题。

四、扩展功能
1. 支持多参数

若需注入多个参数,可将注解改为数组形式:

java 复制代码
@JsonTests({
    @JsonTest(parameterName = "person", jsonValue = "..."),
    @JsonTest(parameterName = "address", jsonValue = "...")
})
@Test
void testMultipleParams(Person person, Address address) { ... }

BeforeEachCallback 中遍历 @JsonTests 注解,解析每个 @JsonTest 并存储参数值。

java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class JsonParameterPreprocessor implements BeforeEachCallback {
    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        Method testMethod = context.getRequiredTestMethod();
        
        // 检查是否带有 @JsonTests 注解
        if (testMethod.isAnnotationPresent(JsonTests.class)) {
            JsonTests annotations = testMethod.getAnnotation(JsonTests.class);
            
            // 遍历每个 @JsonTest 注解
            for (JsonTest annotation : annotations.value()) {
                String parameterName = annotation.parameterName();
                String jsonValue = annotation.jsonValue();

                // 遍历方法参数,匹配参数名
                for (Parameter param : testMethod.getParameters()) {
                    if (param.getName().equals(parameterName)) {
                        // 将 JSON 转换为对象
                        Object parameterValue = objectMapper.readValue(jsonValue, param.getType());
                        
                        // 存储到上下文
                        context.getStore(ExtensionContext.Namespace.GLOBAL)
                              .put(parameterName, parameterValue);
                        break;
                    }
                }
            }
        }
    }
}
2. 从文件加载 JSON

修改 JsonParameterPreprocessor,支持 file: 前缀:

java 复制代码
String jsonValue = annotation.jsonValue();
if (jsonValue.startsWith("file:")) {
    String path = jsonValue.substring("file:".length());
    jsonValue = Files.readString(Paths.get(path));
}

五、总结

通过结合 BeforeEachCallbackParameterResolver,实现了以下功能:

  • 方法级注解:在测试方法上直接指定参数名和 JSON 数据。
  • 自动类型转换:将 JSON 字符串转换为复杂 Java 对象。
  • 灵活扩展:支持文件加载、多参数等场景。

优势

  • 代码简洁:测试方法参数与 JSON 数据解耦。
  • 高可读性:注解明确指定数据来源和参数映射。
  • 兼容 JUnit 5:严格遵循扩展模型,无侵入式设计。

适用场景

  • API 测试:直接注入请求参数对象。
  • 数据驱动测试:从外部 JSON 文件加载测试用例。
  • 复杂对象验证:嵌套 JSON 结构映射到领域模型。
相关推荐
怡人蝶梦1 小时前
Java后端技术栈问题排查实战:Spring Boot启动慢、Redis缓存击穿与Kafka消费堆积
java·jvm·redis·kafka·springboot·prometheus
瓯雅爱分享1 小时前
MES管理系统:Java+Vue,含源码与文档,实现生产过程实时监控、调度与优化,提升制造企业效能
java·mysql·vue·软件工程·源代码管理
鬼多不菜2 小时前
一篇学习CSS的笔记
java·前端·css
深色風信子2 小时前
Eclipse 插件开发 5.3 编辑器 监听输入
java·eclipse·编辑器·编辑器 监听输入·插件 监听输入
Blossom.1182 小时前
人工智能在智能健康监测中的创新应用与未来趋势
java·人工智能·深度学习·机器学习·语音识别
shangjg32 小时前
Kafka 如何保证不重复消费
java·分布式·后端·kafka
无处不在的海贼2 小时前
小明的Java面试奇遇之互联网保险系统架构与性能优化
java·面试·架构
Layux3 小时前
flowable候选人及候选人组(Candidate Users 、Candidate Groups)的应用包含拾取、归还、交接
java·数据库
Mylvzi3 小时前
Spring Boot 中 @RequestParam 和 @RequestPart 的区别详解(含实际项目案例)
java·spring boot·后端