协程与单元测试:为何测试返回两个完全相同的列表?
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
相关产品推荐
相关产品推荐

