代码之家  ›  专栏  ›  技术社区  ›  skaffman

如何测试复杂对象图的相等性?

  •  23
  • skaffman  · 技术社区  · 16 年前

    假设我有一个单元测试想要比较两个复杂的对象是否相等。这些对象包含许多其他深度嵌套的对象。所有对象的类都已正确定义 equals() 方法。

    这并不难:

    @Test
    public void objectEquality() {
        Object o1 = ...
        Object o2 = ...
    
        assertEquals(o1, o2);
    }
    

    问题是,如果对象不相等,你得到的只是一个失败,没有迹象表明对象图的哪个部分不匹配。调试这可能是痛苦和令人沮丧的。

    我目前的方法是确保一切都能实现 toString() ,然后比较以下等式:

        assertEquals(o1.toString(), o2.toString());
    

    这使得跟踪测试失败变得更容易,因为像Eclipse这样的IDE有一个特殊的可视比较器来显示失败测试中的字符串差异。本质上,对象图是以文本形式表示的,因此您可以看到差异在哪里。只要 托斯特林() 写得好,效果很好。

    不过,这一切都有点笨拙。有时,您希望设计toString()用于其他目的,例如日志记录,也许您只想呈现一些对象字段而不是全部对象字段,或者可能根本没有定义toString(),等等。

    我正在寻找一种更好的比较复杂对象图的方法。有什么想法吗?

    9 回复  |  直到 10 年前
        1
  •  8
  •   Brian Agnew    16 年前

    您可以做的是使用 XStream 然后使用 XMLUnit 对XML执行比较。如果它们不同,那么您将得到上下文信息(以xpath、iirc的形式),告诉您对象的不同之处。

    例如,来自xmlunit文档:

    Comparing test xml to control xml [different] 
    Expected element tag name 'uuid' but was 'localId' - 
    comparing <uuid...> at /msg[1]/uuid[1] to <localId...> at /msg[1]/localId[1]
    

    请注意,xpath指示不同元素的位置。

    可能不是很快,但这对于单元测试来说可能不是问题。

        2
  •  10
  •   matt b    16 年前

    这个 Atlassian Developer Blog 有几篇关于这个主题的文章,以及Hamcrest库如何使调试这种测试失败变得非常简单:

    基本上,对于这样的断言:

    assertThat(lukesFirstLightsaber, is(equalTo(maceWindusLightsaber)));
    

    hamcrest将像这样返回输出(其中只显示不同的字段):

    Expected: is {singleBladed is true, color is PURPLE, hilt is {...}}  
    but: is {color is GREEN}
    
        3
  •  4
  •   CPerkins    16 年前

    因为我倾向于设计复杂的对象,所以这里我有一个非常简单的解决方案。

    当设计一个复杂的对象时,我需要为其编写一个equals方法(因此也是一个hashcode方法),我倾向于编写一个字符串呈现器,并使用string类equals和hashcode方法。

    当然,渲染器并不是要字符串化:它不需要人类真正容易阅读,它包含了我需要比较的所有值,而且只包含了我需要比较的值,并且根据习惯,我将它们按控制我希望它们排序方式的顺序排列;对于ToString方法,没有一个值是正确的。

    当然,我缓存这个呈现的字符串(以及hashcode值)。它通常是私有的,但是将缓存的字符串包保留为私有会让您从单元测试中看到它。

    顺便说一下,这并不是我最终在交付的系统中得到的结果,当然-如果性能测试表明这个方法太慢,我准备替换它,但这是一个罕见的情况。到目前为止,这种情况只发生过一次,在一个系统中,易变物体被迅速地改变并经常被比较。

    我这么做的原因是 writing a good hashCode isn't trivial ,并且需要测试(*),而使用字符串中的一个可以避免测试。

    (*考虑到Josh Bloch编写好哈希代码方法的方法的第3步是测试它,以确保“equal”对象具有相等的哈希代码值,并确保覆盖了所有可能的变化本身并不简单。更微妙、更难测试的是分布)

        4
  •  3
  •   John DeRegnaucourt    12 年前

    此问题的代码存在于 http://code.google.com/p/deep-equals/

    使用DePiQual.DeqQuales(A,B)比较两个Java对象的语义相等性。这将使用它们可能拥有的任何自定义equals()方法比较对象(如果它们实现的equals()方法不是object.equals())。如果没有,那么这个方法将继续递归地逐字段比较对象。当遇到每个字段时,如果它存在,它将尝试使用派生的equals(),否则它将继续递归。

    此方法将在这样的循环对象图上工作:a->b->c->a。它具有循环检测功能,因此可以比较任意两个对象,并且永远不会进入无休止的循环。

    使用deepequals.hashcode(obj)为任何对象计算hashcode()。与deepEquals()一样,如果实现了自定义hashcode()方法(在object.hashcode()下面),它将尝试调用hashcode()方法,否则它将逐字段递归(深层)计算hashcode。与deepEquals()一样,此方法将处理具有循环的对象图。例如,a->b->c->a。在这种情况下,hashcode(a)==hashcode(b)==hashcode(c)。deepEquals.deephashcode()具有循环检测功能,因此可以在任何对象图上工作。

        5
  •  1
  •   Ula Krukar    16 年前

    单元测试应该有明确的定义, 单一的 他们测试的东西。这意味着最终你应该有明确的定义, 单一的 这两个物体可能不同。如果有太多的东西可以不同,我建议将这个测试分成几个较小的测试。

        6
  •  1
  •   KLE rslite    16 年前

    我跟你走的是同一条路。我还有其他的麻烦:

    • 我们不能修改我们不拥有(JDK)、数组等的类(对于Equals或ToString)。
    • 在不同的环境中,平等有时是不同的。

    例如,跟踪实体的相等性可能依赖于可用的数据库ID(“同一行”概念),依赖于某些字段(业务键)的相等性(对于未保存的对象)。对于JUnit断言,您可能需要所有字段相等。


    所以我最终创建了通过一个图表运行的对象,并在它们运行的过程中完成它们的工作。

    通常有一个超类 爬行 对象:

    • 在对象的所有属性中爬行;停止于:

      • 枚举,
      • 框架类(如果适用)
      • 在卸载代理或远程连接时,
      • 在已访问的对象处(以避免循环)
      • 在多对一关系中,如果它们表示父级(通常不包括在等号语义中)
    • 可配置,以便它可以在某个点停止(完全停止,或停止在当前属性内爬行):

      • 当mustStopCurrent()或mustStopCompletely()方法返回true时,
      • 在getter或类上遇到一些注释时,
      • 当当前(类、getter)属于异常列表时

    从这个爬行超类中,子类可以满足许多需求:

    • 创建一个 调试字符串 (根据需要调用ToString,对于没有好ToString的集合和数组使用特殊情况;处理大小限制,等等)。
    • 为了创造 几个均衡器 (如前所述,对于使用ID的实体,对于所有字段,或仅基于等于;)。这些均衡器通常也需要特殊情况(例如,对于您控制之外的类)。

    回到问题上来:这些均衡器可以 记住不同值的路径 这对你的JUnit案例理解差异是非常有用的。

    • 为了创造 订货人 . 例如,需要完成的保存实体是一个特定的顺序,效率将指示将相同的类一起保存将带来巨大的提升。
    • 用于收集可以在图中的不同级别找到的一组对象。循环的结果 收藏家 很容易。

    作为补充,我必须说,除了那些真正关注性能的实体之外,我确实选择了在我的实体上实现ToString()、HashCode()、Equals()和CompareTo()的技术。

    例如,如果一个或多个字段上的业务键是通过类上的@uniqueconstraint在Hibernate中定义的,那么让我们假设所有实体都在公共超类中实现了getIdent()属性。 “我的实体”超类具有依赖于此知识的这4个方法的默认实现,例如(需要注意空值):

    • toString()打印“myClass(key1=value1,key2=value2)”
    • hashcode()是“value1.hashcode()^value2.hashcode()”
    • equals()是“value1.equals(other.value1)&value2.equals(other.value2)”。
    • compareTo()结合了类、值1和值2的比较。

    对于关注性能的实体,我只是重写这些方法,不使用反射。我可以在回归JUnit测试中测试这两个实现的行为是否相同。

        7
  •  0
  •   RMorrisey    16 年前

    我们使用一个名为junitx的库来测试所有“公共”对象上的equals契约: http://www.extreme-java.de/junitx/

    我能想到的测试equals()方法的不同部分的唯一方法是将信息分解成更细粒度的内容。如果您正在测试一个深度嵌套的对象树,那么您所做的并不是真正的单元测试。您需要使用该类型对象的单独测试用例来测试图中每个单独对象的equals()契约。对于被测试对象上的类类型字段,可以使用stub对象和simplistic equals()实现。

    高温高压

        8
  •  0
  •   SingleShot    16 年前

    我不会用 toString() 因为正如您所说,为了显示或记录的目的,创建对象的良好表示通常更有用。

    在我看来,你的“单元”测试并没有隔离被测单元。例如,如果对象图是 A-->B-->C 你在测试 A ,您的单元测试 不应该在意 equals() 方法在 C 正在工作。您的单元测试 C 会确保它工作。

    所以我会在测试中测试以下内容 等式() 方法: -比较两个相同的对象 B 在两个方向,例如 a1.equals(a2) a2.equals(a1) . 比较两个 不同的对象 在两个方向

    这样做,通过对每个比较使用JUnit断言,您将知道失败在哪里。

    显然,如果你的班上有更多的孩子是决定平等的一部分,你需要测试更多的组合。不过,我想知道的是,您的单元测试不应该关心它直接接触的类之外的任何东西的行为。在我的例子中,这意味着,你可以假设 C.equals() 工作正常。

    如果你在比较收藏品,可能会有一个问题。在这种情况下,我将使用一个实用程序来比较集合,例如公共集合 CollectionUtils.isEqualCollection() . 当然,只适用于被测试单元中的集合。

        9
  •  0
  •   Bruno Bieth    10 年前

    如果你愿意用scala编写你的测试,你可以使用 matchete . 它是一个Matchers集合,可以与JUnit一起使用,并提供 compare objects graphs 以下内容:

    case class Person(name: String, age: Int, address: Address)
    case class Address(street: String)
    
    Person("john",12, Address("rue de la paix")) must_== Person("john",12,Address("rue du bourg"))
    

    将生成以下错误消息

    org.junit.ComparisonFailure: Person(john,12,Address(street)) is not equal to Person(john,12,Address(different street))
    Got      : address.street = 'rue de la paix'
    Expected : address.street = 'rue du bourg'
    

    正如您在这里看到的,我一直在使用case类,Matchete可以识别这些类,以便深入到对象图中。 这是通过一个名为 Diffable .我不打算在这里讨论类型类,所以假设它是这个机制的基石,它比较了给定类型的两个实例。不是case类的类型(基本上所有爪哇中的所有类型)都会得到默认值 可区分的 使用 equals . 这不是很有用,除非你提供 可扩散的 对于您的特定类型:

    // your java object
    public class Person {
       public String name;
       public Address address;
    }
    
    // you scala test code
    implicit val personDiffable : Diffable[Person] = Diffable.forFields(_.name,_.address)
    
    // there you go you can now compare two person exactly the way you did it
    // with the case classes
    

    所以我们看到MatCHETE与Java代码库工作得很好。事实上,我在一个大型Java项目上的最后一个工作中一直使用MatCHETE。

    免责声明:我是Matchete的作者:)