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

协程与单元测试:为何测试返回两个完全相同的列表?

Kotlin单元测试中两个用户列表始终相同的问题排查与修复

你的测试中两个用户的HumanValue列表实际上是同一个对象引用,导致所有操作都在同一个集合上执行,最终结果必然完全一致,以下是具体问题分析和修复方案:

核心问题分析

1. 共享的列表引用

ValueRepo中的freshDeckObject()直接返回类内部的hvObjectList实例,这是一个全局的ArrayList。每次调用该方法都会返回同一个列表的引用,所以userOne和userTwo指向的是同一个集合——修改其中一个的元素,另一个会同步变化。

2. 可变对象的直接修改

HumanValue是包含var属性的data class,所有修改操作(比如gamesPlayed++)都是直接修改原对象的属性,结合共享引用的问题,两个用户的操作本质上是在修改同一批对象。

3. 无意义的异步操作

测试中用async并行执行游戏逻辑,但因为两个用户共享列表,并行操作反而可能引发竞态条件,且这个场景完全不需要异步,同步执行更合理。


修复方案

1. 让每个用户获得独立的列表副本

修改ValueRepo的freshDeckObject()方法,每次返回新的列表副本,确保用户间的列表完全独立:

class ValueRepo {
    // 返回新的ArrayList副本,避免共享引用
    fun freshDeckObject() = ArrayList(hvObjectList)

    private val hvObjectList = arrayListOf(
        HumanValue(0, "acceptance", 0),
        HumanValue(1, "accomplishment", 1),
        // 其余250个元素...
    )

    fun drawTwo(valueList: List<HumanValue>): List<HumanValue> {
        val splitList = valueList.groupBy { it.priority }
        val average = splitList.values.map { list -> list.map { value -> value.gamesPlayed }.average() }.average()
        val topHalf = valueList.filter { it.gamesPlayed <= average }.sortedBy { it.priority }.chunked(2).flatten()
        Logger.d(topHalf)
        return topHalf.shuffled().take(2)
    }
}

2. (可选)改用不可变对象+复制模式优化

如果想避免直接修改对象属性的副作用,可以将HumanValue改为不可变结构,通过copy方法更新状态:

// 改为不可变data class,所有属性用val修饰
data class HumanValue(
    val id: Int = 0,
    val name: String? = "error",
    val priority: Int = 9,
    val gamesPlayed: Int = 0,
    val gamesWon: Int = 0,
    val gamesLost: Int = 0,
    val gamesTied: Int = 0,
    val rating: Int = 0,
    val kFactor: Double = 0.0
)

对应的runGame方法修改为:

private fun runGame(list: List<HumanValue>): List<HumanValue> {
    val drawn = repo.drawTwo(list)
    val winnerId = drawn[0].id
    val loserId = drawn[1].id

    return list.map { value ->
        when (value.id) {
            winnerId -> value.copy(
                gamesPlayed = value.gamesPlayed + 1,
                gamesWon = value.gamesWon + 1
            )
            loserId -> value.copy(
                gamesPlayed = value.gamesPlayed + 1,
                gamesLost = value.gamesLost + 1
            )
            else -> value
        }
    }
}

3. 简化测试的执行逻辑

去掉不必要的异步操作,同步执行游戏循环:

@Test
fun testDeck() {
    var userOne = repo.freshDeckObject()
    var userTwo = repo.freshDeckObject()

    // 同步执行100次游戏
    repeat(100) {
        println("Ran $it games")
        userOne = runGame(userOne) as ArrayList<HumanValue>
        userTwo = runGame(userTwo) as ArrayList<HumanValue>
    }

    println("Starting job 2")
    val existing1 = userOne.filter { it.gamesPlayed > 0 }.map { it.id }
    val existing2 = userTwo.filter { it.gamesPlayed > 0 }.map { it.id }
    val mutual = existing1.intersect(existing2.toSet())

    assertTrue(existing1.isNotEmpty())
    assertTrue(existing2.isNotEmpty())
    assertTrue(mutual.isNotEmpty())
    assertNotEquals(userOne, userTwo) // 现在该断言会通过
}

内容的提问来源于stack exchange,提问作者Paxana Non Grata

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 02:17:29