Skip to content

Rain 应用测试

rain-test 在 JUnit 5 中建立 Rain AppClassloader 和完整应用上下文,使 DI、Loader、Event、Job、Hook 等能力按接近生产启动的方式运行。

添加依赖

kotlin
dependencies {
    testImplementation("com.IceCreamQAQ.Rain:rain-test:1.0.0-DEV12")
}

tasks.test {
    useJUnitPlatform()
}

rain-test 传递依赖 Application 和 JUnit Jupiter API,并通过 META-INF/services/org.junit.platform.engine.TestEngine 注册 rain.test.RainTestEngine

最小测试

kotlin
@RainTest
class UserServiceTest(
    private val userService: UserService,
) {
    @Test
    fun createsUser() {
        val user = userService.create("Rain")
        assertEquals("Rain", user.name)
    }
}

@RainTest 同时注册 RainTestExtension。Extension 启动应用,再要求 DiContext 创建测试类实例,因此构造器和字段注入均可使用。

kotlin
@RainTest
class FieldInjectionTest {
    @Inject
    lateinit var service: UserService
}

构造器注入更容易看出测试依赖,也便于不启动 Rain 的纯单元测试复用。

测试启动过程

text
JUnit Platform 发现测试

RainTestEngine 创建 AppClassloader

标记 @RainTest 的描述符替换为 AppClassloader 中的 Class/Method

RainTestExtension 调用 TestApplicationStarter

注册 EnchantManager 与 HookImpl

Application.start()

从 DiContext 获取测试类 Bean

Jupiter 执行测试方法

为什么 TestEngine 和 Extension 都需要?

  • Engine 确保测试类本身由 AppClassloader 重新加载,字节码增强可生效。
  • Extension 确保测试实例来自 Rain DI,而不是 Jupiter 默认构造器。

只写一个普通 JUnit Extension 不能解决“测试类已被系统类加载器加载”的问题。

测试配置

使用测试资源目录:

text
src/test/resources/conf/
├─ application.properties
└─ test/
   └─ application.properties

基础配置:

properties
rain.scanPackages=com.example

通过系统属性固定模式:

kotlin
tasks.test {
    systemProperty("rain.runMode", "test")
}

然后把数据库、端口、外部服务地址放在 conf/test 覆盖中。不要让测试默认连接生产资源。

测试 Event

kotlin
@RainTest
class EventTest(
    private val eventBus: EventBus,
    private val recorder: OrderEventRecorder,
) {
    @Test
    fun recordsOrder() {
        eventBus.post(OrderCreated(1))
        assertEquals(listOf(1L), recorder.ids)
    }
}

EventBus 是同步的,普通监听器断言不需要等待。监听器自行异步化时,测试必须使用可控 dispatcher、Latch 或超时等待。

测试 Job

kotlin
@RainTest
@CronTab
class JobTest {
    @Volatile
    var ran = false

    @Cron("50ms")
    fun tick() {
        ran = true
    }

    @Test
    fun runsJob() = runBlocking {
        withTimeout(2_000) {
            while (!ran) delay(10)
        }
    }
}

使用超时而不是固定长 sleep,失败更快且避免无限等待。Job 线程在 Application shutdown 时关闭;当前测试生命周期是否每类关闭应用需要结合版本验证。

测试 Hook 与事务

Hook 测试必须验证目标类确实从 AppClassloader 加载:

kotlin
@Test
fun usesRainClassloader() {
    assertTrue(service.javaClass.classLoader is AppClassloader)
}

事务测试应使用真实测试数据库并同时验证:

  1. 正常返回时提交。
  2. 抛异常时回滚。
  3. Catch 后吞异常时是否按预期提交。
  4. 多次运行是否遗留 ThreadLocal/EntityManager 状态。

不要用 mock EntityManager 就声称声明式事务已正确工作;需要数据库可观察状态作为证据。

普通单元测试与 RainTest 的边界

不是所有测试都应标 @RainTest

kotlin
class PriceCalculatorTest {
    private val calculator = PriceCalculator(FakeRateProvider())

    @Test
    fun calculates() = Unit
}

推荐:

  • 纯函数、领域对象、手工可构造服务:普通 JUnit。
  • 需要 DI 装配、Loader 扫描、Event/Job/Hook:@RainTest
  • 数据库、SmartWeb Server:@RainTest + 外部资源/端到端测试。

RainTest 启动完整应用,速度和隔离性都不如单元测试。

测试类扫描

测试类也必须位于 rain.scanPackages,因为 Extension 最终通过 context.getBean(testClass) 创建实例。测试包不在扫描范围时,仍可能按需创建具体类,但依赖的监听器、Job、Controller 等扫描组件不会自动注册。

建议显式加入生产根包和测试 fixture 包:

properties
rain.scanPackages=[com.example.app, com.example.testfixture]

当前实现限制

源码显示以下边界:

  • RainTestExtension 初始化时立即启动 Application,没有公开按测试类定制启动参数的 API。
  • beforeTestExecution 当前为空。
  • Extension 没有显式调用 Application.stop();长期测试进程中要关注线程与资源释放。
  • Engine 对 Jupiter 内部 descriptor 类型有直接依赖,升级 JUnit Platform 版本可能产生兼容问题。
  • TestApplicationStarter 中 Kotlin ByInject Transformer 注册被注释;委托注入是否与生产一致必须针对当前版本验证。
  • 并行执行多个 RainTest 可能共享/竞争线程上下文类加载器和外部资源,不建议默认开启。

Gradle 故障排查

找不到测试

确认:

  • useJUnitPlatform() 已开启。
  • rain-test 位于 test runtime classpath。
  • TestEngine 服务文件没有被打包排除。
  • 没有用版本冲突的 JUnit Engine 覆盖 Rain 声明版本。

ClassCastException:类名相同

通常是测试方法、参数或依赖对象分别由系统类加载器和 AppClassloader 加载。不要在静态初始化中提前引用需要增强的应用类,并检查测试框架插件是否缓存 Class。

测试结束进程不退出

检查 Job、Web Server、数据库池和自建线程是否由 ApplicationService.stop 管理。当前 rain-test 关闭行为有限,必要时在测试套件级别显式清理外部资源。

测试分层建议

text
大量普通 JUnit 单元测试

少量 @RainTest 模块集成测试

SmartWeb HTTP / SmartAccess 数据库端到端测试

Rain 的简单实现让集成测试能覆盖真实 Loader 和增强路径;也正因为大量行为在启动期发生,只有单元测试无法证明框架集成正确。

基于 Apache License 2.0 发布