Kotlin Result密封接口Error实例assertEquals断言失败问题
问题根因
首先明确两个核心前提:
- Kotlin
data class自动生成的equals()逻辑,是逐一对主构造函数内声明的属性调用equals()做比较,所有属性判断相等时,整个实例才判定相等。 - JVM 中的
Throwable及其所有子类(包括你用到的RuntimeException)没有重写equals()和hashCode(),直接继承了Any的默认实现:仅当两个引用指向堆内存中同一个对象时才判定相等,也就是引用相等,和异常的message、栈信息、cause等内容无关。
这就解释了你遇到的两个现象:
- 为什么Success场景断言能通过:
Success持有的data属性一般是基础类型、String、自定义data class这类重写了值相等逻辑的类型,比较时会匹配内容,自然能通过。 - 为什么Error场景断言失败、但
toString()比较能通过:- Error持有的属性是
Throwable,默认按引用比较。你的测试场景中,协程框架执行时大概率会对抛出的原始异常做包装(比如包装为CompletionException),或者use case内部捕获异常时做了二次封装,导致expected中存的异常实例和actual中存的异常实例不是同一个内存对象,哪怕message、类型完全一致,equals()也会返回false。 Throwable重写了toString()方法,会输出异常类名、message等文本内容,只要内容一致字符串就相等,和实例引用无关,所以这个断言能通过。
- Error持有的属性是
可行解决方案
你可以根据自己的项目场景选以下任意一种方案:
- 方案1:重写Error类的
equals()和hashCode(),按异常的核心内容做比较,不依赖Throwable默认的引用相等逻辑。参考实现:
data class Error(val exception: Throwable? = null) : Result<Nothing> { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Error) return false val otherExp = other.exception if (exception == null && otherExp == null) return true if (exception == null || otherExp == null) return false // 按业务关心的异常维度比较,一般比较类型、message、根因即可 return exception::class == otherExp::class && exception.message == otherExp.message && exception.cause?.message == otherExp.cause?.message } override fun hashCode(): Int { var result = exception?.javaClass?.hashCode() ?: 0 result = 31 * result + (exception?.message?.hashCode() ?: 0) result = 31 * result + (exception?.cause?.message?.hashCode() ?: 0) return result } }
- 方案2:避免在Result中直接存Throwable,将异常转换为结构化的自定义错误模型。这种方式更符合分层设计原则,不会把数据层/框架层的异常直接透传到上层,自定义的错误模型是普通data class,会自动生成值相等的
equals()逻辑,从根源上避开Throwable的引用相等问题。示例:
// 自定义结构化错误类型 sealed interface BizError { data class HttpError(val code: Int, val msg: String) : BizError data class NetworkError(val msg: String) : BizError object UnknownError : BizError } sealed interface Result<out T> { data class Success<T>(val data: T) : Result<T> data class Error(val error: BizError) : Result<Nothing> }
- 方案3:不修改Result实现,调整单元测试的断言逻辑,不直接比较整个Error实例,而是分步断言类型和异常核心字段:
assertTrue(actual is Result.Error) val actualError = actual as Result.Error assertEquals(exception::class, actualError.exception?.::class) assertEquals("HTTP Error", actualError.exception?.message)
- 方案4:在异常捕获逻辑中统一做解包处理,保证存入Result.Error的异常和抛出的原始异常是同一个实例,这种方式可以让默认的引用相等生效,但非常脆弱,只要中间任意一层做了异常包装就会失效,不推荐在生产代码中使用。
内容的提问来源于stack exchange,提问作者wiryadev
相关产品推荐
相关产品推荐

