在 Spring 配置中实现具有可读属性的自定义注释(测试)

usr*_*ΛΩΝ 4 spring spring-test spring-annotations

我熟悉复合注释。然而,即使经过一些研究,它们似乎还不足以满足我的特定需求。

一般情况

我想创建一个用于测试的注释,将其与一些属性放在一个类上,可以自定义测试上下文。

public @interface MyTestingAnnotation {

    String myTestingAttribute()

}
Run Code Online (Sandbox Code Playgroud)

的值应该由我的自定义 (TBD )myTestingAttribute之一读取@TestConfiguration

实际例子

在我的应用程序中,我必须模拟时钟,以便测试可以模拟特定时间点运行。例如,测试结果不依赖于硬件时钟。我java.time.Clock为此目的定义了一个 bean。

目前,我只有一个注释来启用模拟时钟,但指定其时间取决于@TestPropertySource

public @interface MyTestingAnnotation {

    String myTestingAttribute()

}
Run Code Online (Sandbox Code Playgroud)

相反,我想注释一个测试@MockClock(at = "2024-02-15T12:35:00+04:00"),而不一定使用属性源语法。

我知道如何@AliasFor在自定义注释中使用,但目前我只能@Import(MockClockConfiguration.class)在元注释中使用。

我怎样才能在 Spring 中实现这样的目标?

rie*_*pil 6

您可以使用 aContextCustomizer来达到此目的。

这种方法允许您在运行测试之前修改应用程序上下文,从而提供对上下文配置的细粒度控制,包括添加或覆盖 bean、属性等。

要通过 实现您的目标ContextCustomizer,请按照下列步骤操作:

  1. 创建自定义注释
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MockClock {
    String at();
}
Run Code Online (Sandbox Code Playgroud)
  1. 实施ContextCustomizer

创建ContextCustomizer将根据@MockClock注释修改应用程序上下文的环境或 bean 定义。

import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;

import java.time.OffsetDateTime;

public class MockClockContextCustomizer implements ContextCustomizer {

    private final OffsetDateTime mockTime;

    public MockClockContextCustomizer(OffsetDateTime mockTime) {
        this.mockTime = mockTime;
    }

    @Override
    public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
        TestPropertyValues.of(
                        "clock.fixed-instant=" + mockTime.toString())
                .applyTo(context);
    }

    // MockClockContextCustomizer must implement equals() and hashCode(). See the Javadoc for ContextCustomizer for details.
}
Run Code Online (Sandbox Code Playgroud)
  1. 实施ContextCustomizerFactory

创建一个在测试类上ContextCustomizerFactory查找注释并根据注释的属性生成一个注释的注释。@MockClockContextCustomizer

import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;

import java.time.OffsetDateTime;
import java.util.List;

public class MockClockContextCustomizerFactory implements ContextCustomizerFactory {
    @Override
    public ContextCustomizer createContextCustomizer(Class<?> testClass, List<ContextConfigurationAttributes> configAttributes) {
        MockClock mockClock = testClass.getAnnotation(MockClock.class);
        if (mockClock != null) {
            return new MockClockContextCustomizer(OffsetDateTime.parse(mockClock.at()));
        }
        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 注册ContextCustomizerFactory

Spring 不会自动发现 ContextCustomizerFactory 实现。要注册自定义工厂,您需要META-INF/spring.factories在项目的资源目录中创建一个名为的文件(如果该文件尚不存在)并添加以下行:

org.springframework.test.context.ContextCustomizerFactory=\
your.package.MockClockContextCustomizerFactory
Run Code Online (Sandbox Code Playgroud)

替换your.package为您所在的实际包名称MockClockContextCustomizerFactory

作为替代方案,您可以从 Spring Framework 6.1 开始ContextCustomizerFactory在本地注册 via @ContextCustomizerFactories

怎么运行的

  • @MockClock当执行带有注释的测试类时,Spring Test 会查找ContextCustomizerFactory中指定的实现spring.factories
  • MockClockContextCustomizerFactory检查注释是否存在,如果找到,则创建具有指定固定时刻@MockClock的实例。MockClockContextCustomizer
  • 然后MockClockContextCustomizer在测试运行之前自定义应用程序上下文,将所需的属性添加到环境中,您MockClockConfiguration可以使用它来创建模拟的 Clock bean。