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

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

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

    equals() 方法。

    这并不难:

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

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

    我目前的做法是确保一切都能实现 toString() ,然后像这样比较等式:

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

    toString() 写得很好,效果很好。

    不过,这一切都有点笨拙。有时你想为其他目的设计toString(),比如日志记录,也许你只想渲染一些对象字段而不是所有对象字段,或者可能根本没有定义toStrings(),等等。

    9 回复  |  直到 16 年前
        1
  •  8
  •   Brian Agnew    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}
    
        2
  •  10
  •   matt b    16 年前

    您可以使用以下命令将每个对象渲染为XML 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指示不同元素的位置。

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

        3
  •  4
  •   CPerkins    16 年前

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

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

    当然,渲染器不是toString:它真的不必让人容易阅读,它只包含我需要比较的所有值,而且按照习惯,我把它们按顺序排列,以控制我希望它们排序的方式;对于toString方法,这些都不一定是正确的。

    当然,我缓存了这个渲染的字符串(以及hashCode值)。它通常是私有的,但将缓存的字符串包保留为私有可以让您在单元测试中看到它。

    顺便说一句,当然,这并不总是我在交付的系统中最终得到的结果——如果性能测试表明这种方法太慢,我准备替换它,但这种情况很少见。到目前为止,这种情况只发生过一次,在一个可变对象被快速更改和频繁比较的系统中。

    我这样做的原因是 writing a good hashCode isn't trivial ,并且需要测试(*),而使用String中的一个则避免了测试。

        4
  •  3
  •   John DeRegnaucourt    13 年前

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

    使用DeepEquals.dephEquals(a,b)比较两个Java对象的语义相等性。这将使用它们可能具有的任何自定义equals()方法对对象进行比较(如果它们实现了除Object.equals()之外的equal()方法)。如果没有,此方法将继续逐字段递归比较对象。当遇到每个字段时,如果存在,它将尝试使用派生的equals(),否则它将继续递归。

    此方法适用于循环对象图,如下所示:a->B->C->A.它具有循环检测功能,因此可以比较任何两个对象,并且它永远不会进入无休止的循环。

    使用DeepEquals.hashCode(obj)为任何对象计算hashCode()。与deepEquals()一样,如果实现了自定义hashCode()方法(位于Object.hashCode()下方),它将尝试调用hashCode方法,否则它将逐字段递归计算hashCode。与deepEquals()一样,此方法将处理具有循环的对象图。例如,A->B->C->A.在这种情况下,hashCode(A)==hashCode。DeepEquals.dephHashCode()具有循环检测功能,因此适用于任何对象图。

        5
  •  1
  •   Ula Krukar    16 年前

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

        6
  •  1
  •   KLE rslite    16 年前

    • 我们不能修改我们不拥有的类(for equals或toString)(JDK)、数组等。
    • 平等在不同的环境中有时是不同的

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


    所以我最终创建了贯穿图形的对象,边做边做。

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

    • 遍历对象的所有属性;停在:

      • 枚举,
      • 框架类(如果适用),
      • 在未加载的代理或远程连接处,
      • at已访问的对象(以避免循环)
      • ...
    • 可配置,以便它可以在某个时候停止(完全停止,或停止在当前属性内爬行):

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

    从这个Crawling超类中,为许多需求制作了子类:

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

    回到问题:这些均衡器可以 记住通往不同价值观的道路 ,这将非常有助于您理解JUnit案例中的差异。

    • 用于创建 例如,保存实体需要按照特定的顺序进行,效率将决定将相同的类保存在一起将带来巨大的提升。
    • 用于收集可以在图中不同级别找到的一组对象。循环查看结果 收集者 那么就很容易了。

    作为补充,我必须说,除了那些真正关心性能的实体外,我确实选择了这种技术来在我的实体上实现toString()、hashCode()、equals()和compareTo()。

    • toString()打印“myClass(key1=value1,key2=value2)”
    • hashCode()是“value1.hashCode
    • equals()是“value1.equals和value2.equals”

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

        7
  •  0
  •   RMorrisey    16 年前

    http://www.extreme-java.de/junitx/

    我能想到的测试equals()方法不同部分的唯一方法是将信息分解为更细粒度的信息。如果你正在测试一个嵌套很深的对象树,那么你所做的并不是真正的单元测试。您需要使用针对该类型对象的单独测试用例来测试图中每个单独对象的equals()合约。您可以使用带有简单equals()实现的存根对象来处理被测对象上的类类型字段。

        8
  •  0
  •   SingleShot    16 年前

    我不会使用 toString() 因为正如你所说,它通常更有助于为显示或日志记录目的创建对象的良好表示。

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

    因此,我将在测试中测试以下内容 A. s equals() 方法: -比较两个具有相同属性的A对象 B 在两个方向上,例如。 a1.equals(a2) a2.equals(a1) . A. 具有不同 B 的,在两个方向上

    通过这种方式,每次比较都使用JUnit断言,您将知道故障在哪里。

    C.equals()

    一个可能的问题是,如果你在比较系列。在这种情况下,我会使用一个实用程序来比较集合,比如公共集合 CollectionUtils.isEqualCollection() 。当然,仅适用于测试单元中的集合。

        9
  •  0
  •   Bruno Bieth    10 年前

    如果你愿意用scala编写测试,你可以使用 matchete 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'
    

    Diffable 我不打算在这里讨论类型类,所以假设它是这个机制的基石,它比较给定类型的2个实例。非case类的类型(基本上Java中的所有类型)都会得到默认值 差异化 使用 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。