如何解决org.assertj.core.api.ObjectAssert.usingRecursiveComparison() 配置不用于子对象
我们的团队正在使用 Kotlin 和 Springboot 构建使用 Google Cloud Spanner 作为数据存储区的网络服务。
Spanner 有一个很酷的特性,叫做 commit timestamps,它记录一行在指定列中提交到数据库的时间。
这个特性的问题在于它使得编写单元测试变得困难。特别是,我想在测试中编写如下所示:
@Test
fun saveAndLoadTest() {
val id = UUID.randomUuid().toString()
val expected = Foo(id)
dao.save(expected)
val actual = dao.load(id)
assertThat(actual).isEqualTo(expected)
}
但是如果 Foo
有一个由 Spanner 的提交时间戳填充的字段,这将不起作用,因为当 actual
与 expected
进行比较时,该字段会有所不同。
@Table("Foo")
data class Foo (
@PrimaryKey
val id: String,// the value of this field will differ before and after the object is written to the database
@Column(spannerCommitTimestamp = true)
val createdTimestamp: Timestamp = Timestamp.now()
)
为了解决这个问题,我编写了一个扩展函数来配置比较 Foo
的两个实例的方式。它看起来像这样:
fun <T> ObjectAssert<T>.isEqualIgnoringTimestamps(other: T): ObjectAssert<T> {
this.usingRecursiveComparison()
.ignoringFieldsMatchingRegexes("createdTimestamp.*")
.isEqualTo(other)
return this
}
如果我更新单元测试以使用我的扩展函数,则断言通过,因为名称以 createdTimestamp
开头的任何字段都将被忽略:
@Test
fun saveAndLoadTest() {
val id = UUID.randomUuid().toString()
val expected = Foo(id)
dao.save(expected)
val actual = dao.load(id)
// at this point,we're basically only checking that the `id` fields are the same
assertThat(actual).isEqualIgnoringTimestamps(expected)
}
这很好,但似乎 org.assertj.core.api.ObjectAssert.usingRecursiveComparison()
配置不用于子对象。这意味着如果我把 Foo
复杂化一点,扩展函数就会停止工作:
@Table("Foo")
data class Foo (
@PrimaryKey
val id: String,// the "recursive" comparison rules won't be applied to this child object
val child: Foo,@Column(spannerCommitTimestamp = true)
val createdTimestamp: Timestamp = Timestamp.now()
)
现在单元测试失败并显示如下错误消息:
Expecting:
<Foo(id=adfsikp87r6yu0hwy4pyg8oi9,child=Foo(id=cs6ne8m8tilcbhxq3s4ejxn5n,child=null,createdTimestamp=2021-06-18T20:08:37.142614000Z),createdTimestamp=2021-06-18T20:08:37.142614000Z)>
to be equal to:
<Foo(id=adfsikp87r6yu0hwy4pyg8oi9,createdTimestamp=2021-06-18T20:08:37.352000000Z),createdTimestamp=2021-06-18T20:08:37.352000000Z)>
when recursively comparing field by field,but found the following difference:
field/property 'child' differ:
- actual value : Foo(id=cs6ne8m8tilcbhxq3s4ejxn5n,createdTimestamp=2021-06-18T20:08:37.352000000Z)
- expected value : Foo(id=cs6ne8m8tilcbhxq3s4ejxn5n,createdTimestamp=2021-06-18T20:08:37.142614000Z)
The recursive comparison was performed with this configuration:
- the fields matching the following regexes were ignored in the comparison: createdTimestamp.*
- these types were compared with the following comparators:
- java.lang.Double -> DoubleComparator[precision=1.0E-15]
- java.lang.Float -> FloatComparator[precision=1.0E-6]
- actual and expected objects and their fields were compared field by field recursively even if they were not of the same type,this allows for example to compare a Person to a PersonDto (call strictTypeChecking(true) to change that behavior).
这里的问题似乎是在比较过程中没有忽略 child.createdTimestamp
字段,即使我已经告诉比较器忽略名为 createdTimestamp
的字段。当然,它们在顶层被忽略——只是不在子对象中。
请注意,如果我调整递归比较配置以忽略基于对象类型而不是名称的字段,则会出现同样的问题:
fun <T> ObjectAssert<T>.isEqualIgnoringTimestamps(other: T): ObjectAssert<T> {
this.usingRecursiveComparison()
.ignoringFieldsOfTypes(Timestamp::class.java)
.isEqualTo(other)
return this
}
无论哪种方式,createdTimestamp
字段在具有对象中都会被忽略,但在 has-a 对象中不会被忽略。
所以,抛开所有这些解释:我应该如何处理这个问题?
- 是否有更好的方法来配置递归比较,以便将配置应用于子对象(我本来认为是这种情况,因为它应该是递归,但我想我错了)?
- 在使用提交时间戳时,是否有更好的模式可用于单元测试?我想我可以覆盖
.equals(...)
上的.hashCode()
和Foo
方法,这样createdTimestamp
字段被忽略,但随后我将测试代码泄漏到生产代码中,这使得我觉得恶心
tl;dr: 我的数据库在提交时在我的对象上填充一个创建的时间戳。这使得编写单元测试变得棘手,因为对象在保存时会发生变化。我该如何解决这个问题?
解决方法
我认为您的问题来自两件事的结合:
- 使用旧版本的 AssertJ,在递归比较中默认情况下不会忽略覆盖的等于
- 没有使用正确的正则表达式
错误表明 child
字段不相等,这向我表明您的 Foo
类具有覆盖的 equals
方法(可能来自数据类)并且您从 3.17.0 开始使用 AssertJ Core 版本 equals,递归比较也不会使用它,而是比较它的字段(注意,默认情况下总是比较根对象逐个字段)。
如果您坚持使用旧版本,只需使用 ignoringAllOverriddenEquals。
然后第二个问题是您的正则表达式没有按照您的预期执行,而不是 createdTimestamp.*
,请尝试 .*createdTimestamp
。
这是为什么?字段由它们从根对象(即被测对象)的位置表示,因此当您指定 createdTimestamp.*
时,它意味着以 createdTimestamp
开头的根 对象 的任何字段,它并不意味着任何 createdTimestamp
字段。
在您的示例中,它匹配 Foo.createdTimestamp
,因为 Foo
是要比较的根类型,但它不匹配 Foo.child.createdTimestamp
,要忽略后者,您必须指定 child\.createdTimestamp
( \.
用于正则表达式不将 .
解释为任何字符)。
这并不能真正解决您的问题,因为现在 child.child.createdTimestamp
不会被忽略,但这是正则表达式很方便的地方,.*createdTimestamp
可以完成这项工作,因为它意味着:“任何以 {{1} 结尾的字段位置}}”。
相关文档在这里:
- https://assertj.github.io/doc/#assertj-core-recursive-comparison-ignoring-fields
- https://www.javadoc.io/doc/org.assertj/assertj-core/latest/org/assertj/core/api/RecursiveComparisonAssert.html#ignoringFieldsMatchingRegexes(java.lang.String...)
我想文档可以显示更多关于字段正则表达式用法及其解释方式的示例。 WDYT?
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。