org.assertj.core.api.ObjectAssert.usingRecursiveComparison() 配置不用于子对象

org.assertj.core.api.ObjectAssert.usingRecursiveComparison() Configuration Isn't Used for Child Objects

我们的团队正在使用 Kotlin 和 Springboot 构建 Web 服务,该服务使用 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 的提交时间戳填充的字段,这将不起作用,因为当 actualexpected 进行比较时,该字段会有所不同。

@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, child=Foo(id=cs6ne8m8tilcbhxq3s4ejxn5n, child=null, 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, child=null, createdTimestamp=2021-06-18T20:08:37.352000000Z)
- expected value : Foo(id=cs6ne8m8tilcbhxq3s4ejxn5n, child=null, 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 字段在 having 对象中被忽略,但在 has-a 对象中不会被忽略。

所以,所有这些解释都已解决:我应该如何处理这个问题?

tl;dr: 我的数据库在提交时在我的对象上填充一个创建的时间戳。这使得编写单元测试变得棘手,因为对象在保存时会发生变化。我该如何解决这个问题?

我认为您的问题是由两件事共同造成的:

  • 使用旧版本的 AssertJ,在递归比较中默认不忽略被覆盖的等于
  • 没有使用正确的正则表达式

错误显示 child 字段不相等,这向我表明你的 Foo class 有一个覆盖的 equals 方法(可能来自数据 class) 并且您使用的是从 3.17.0 起 < 3.17.0 的 AssertJ Core 版本,即使 class 已覆盖 equals 递归比较也不会使用它并且将比较它的字段(请注意,默认情况下,根对象总是逐个字段进行比较)。 如果您坚持使用旧版本,只需使用 ignoringAllOverriddenEquals.

那么第二个问题是你的正则表达式没有按照你的预期去做,而不是 createdTimestamp.*,试试 .*createdTimestamp.

这是为什么?字段由它们从根对象(即被测对象)的位置表示,因此当您指定 createdTimestamp.* 时,它意味着根 object 的任何字段以 [=17= 开头], 它不代表任何 createdTimestamp 个字段。

在您的示例中,它匹配 Foo.createdTimestamp,因为 Foo 是正在比较的根类型,但它不匹配 Foo.child.createdTimestamp,要忽略后者,您必须指定 child\.createdTimestamp\. 用于正则表达式不将 . 解释为任何字符)。

这并没有真正解决你的问题,因为现在 child.child.createdTimestamp 没有被忽略,但这是正则表达式很方便的地方,.*createdTimestamp 可以完成这项工作,因为它意味着:“任何以 createdTimestamp".

相关文档在这里:

我想文档可以显示更多字段正则表达式用法示例以及它们的解释方式。 WDYT?