You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在本地连续多次运行单个Compose UI测试?

针对Composable UI测试的Flaky测试重复执行方案

方法一:自定义@Repeat注解+JUnit 5扩展

对于Jetpack Compose UI测试(基于JUnit 5),可以通过自定义注解和Extension实现类似@Repeat(n times)的重复执行逻辑,无需依赖Espresso。

首先定义重复执行的注解:

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Repeat(val times: Int)

然后实现JUnit 5 Extension来处理重复逻辑:

class RepeatExtension : BeforeTestExecutionCallback, AfterTestExecutionCallback {
    private var currentRunCount = 0

    override fun beforeTestExecution(context: ExtensionContext) {
        currentRunCount = 0
    }

    override fun afterTestExecution(context: ExtensionContext) {
        val repeatAnnotation = context.requiredTestMethod.getAnnotation(Repeat::class.java) ?: return
        val totalRuns = repeatAnnotation.times

        if (currentRunCount < totalRuns - 1) {
            currentRunCount++
            // 重新调用测试方法
            context.requiredTestMethod.invoke(context.requiredTestInstance)
        } else {
            // 最后一次执行后,若有失败则抛出汇总错误
            context.executionException.orElse(null)?.let {
                throw AssertionError("测试在$totalRuns次执行中失败", it)
            }
        }
    }
}

在测试类中注册Extension并使用注解:

@ExtendWith(RepeatExtension::class)
class FlakyComposableTests {
    private val composeTestRule = createComposeRule()

    @Test
    @FlakyTest
    @Repeat(times = 15)
    fun testFlakyComponent() {
        composeTestRule.setContent {
            MyFlakyComposable()
        }
        // 你的Compose测试断言
        composeTestRule.onNodeWithText("Target Content").assertIsDisplayed()
    }
}

方法二:测试方法内嵌入循环

如果不想编写扩展,直接在测试方法内部加入循环,手动控制重复次数,同时注意重置测试状态:

class FlakyComposableTests {
    private val composeTestRule = createComposeRule()

    @Test
    @FlakyTest
    fun testRepeatedFlakyComponent() {
        val totalAttempts = 20
        var failureCount = 0

        repeat(totalAttempts) { attempt ->
            runCatching {
                composeTestRule.setContent {
                    MyFlakyComposable()
                }
                // 执行测试断言
                composeTestRule.onNodeWithText("Target Content").assertIsDisplayed()
            }.onFailure {
                failureCount++
                println("第${attempt + 1}次执行失败:${it.localizedMessage}")
            }
            // 重置Compose测试环境,避免状态污染
            composeTestRule.waitForIdle()
        }

        check(failureCount == 0) { "测试在$totalAttempts次执行中失败了$failureCount次" }
    }
}

方法三:通过Gradle参数动态控制重复次数

如果需要灵活调整重复次数,可通过Gradle系统属性传递参数,在测试中读取并执行循环:

在build.gradle.kts中配置测试任务:

tasks.withType<Test> {
    systemProperty("test.repeat.count", project.findProperty("repeatCount") ?: 1)
}

测试方法中读取参数并执行:

class FlakyComposableTests {
    private val composeTestRule = createComposeRule()

    @Test
    @FlakyTest
    fun testFlakyComponentWithGradleParam() {
        val totalRuns = System.getProperty("test.repeat.count").toInt()
        var failureCount = 0

        repeat(totalRuns) { attempt ->
            runCatching {
                composeTestRule.setContent {
                    MyFlakyComposable()
                }
                composeTestRule.onNodeWithText("Target Content").assertIsDisplayed()
            }.onFailure {
                failureCount++
                println("Attempt ${attempt + 1} failed: ${it.message}")
            }
            composeTestRule.waitForIdle()
        }

        require(failureCount == 0) { "Failed $failureCount out of $totalRuns attempts" }
    }
}

执行测试时通过Gradle参数指定次数:

./gradlew test -PrepeatCount=10

注意事项

  • 每次重复执行前必须调用waitForIdle()或重置Compose内容,避免前一次测试的状态残留影响结果。
  • 记录每次失败的详细日志,有助于定位Flaky测试的根因(如异步加载时机、状态竞争等)。
  • 使用JUnit扩展时,确保项目依赖的JUnit 5版本与扩展兼容。

内容的提问来源于stack exchange,提问作者Gianluca Veschi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 13:20:32