2009-09-11 43 views
22

假設我有一個單元測試,想比較兩個複雜的對象是否相等。這些對象包含許多其他深度嵌套的對象。所有對象的類都已正確定義equals()方法。如何測試複雜對象圖的相等性?

這並不難:

@Test 
public void objectEquality() { 
    Object o1 = ... 
    Object o2 = ... 

    assertEquals(o1, o2); 
} 

麻煩的是,如果對象是不相等的,你得到的是一個失敗,沒有指出哪個對象圖的一部分不匹配。調試這可能是痛苦和令人沮喪的。

我目前的做法是,以確保一切實現toString(),然後比較像這種平等:

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

這使得它更容易追查測試失敗,因爲像Eclipse IDE中有一個特殊的視覺比較在失敗測試中顯示字符串差異。本質上,對象圖表是用文本表示的,所以你可以看到差異在哪裏。只要toString()寫得很好,它的效果很好。

雖然這一切都有點笨拙。有時你想爲其他目的設計toString(),比如記錄,也許你只想渲染一些對象字段而不是所有的對象,或者可能根本沒有定義toString()等等。

我正在尋找比較複雜對象圖的更好方法的想法。有什麼想法嗎?

+2

+1好你brough了。我想看看其他人找到的解決方案。 – KLE 2009-09-11 15:56:36

回答

8

您可以做的是使用XStream將每個對象呈現爲XML,然後使用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。

也許並不快,但可能不會對單元測試的問題。

+0

+1我喜歡這個...類似的方法來比較'toString()',而不需要'toString()'。然而,我懷疑堅持使用字符串比較和IDE支持會更容易。 – skaffman 2009-09-11 15:44:10

+1

我認爲你可以很容易地編寫一個名爲assertSameDeeply()或類似的實用程序方法,它將是完全通用的。只需靜態導入並像所有其他JUnit一樣使用它。 – 2009-09-11 15:50:00

+0

我喜歡這個解決方案,因爲比較結果格式良好。不過,我相信馬特b的Hamcrest指針非常好,解決方案聞起來更好。 – Kariem 2010-08-16 10:33:48

1

我跟着你在同一軌道上。我也有過其它附加的煩惱:

  • 我們不能修改類(相等於或的toString),我們不擁有(JDK),陣列等
  • 平等是在各種情況下有時會有所不同

例如,跟蹤實體相等性可能依賴於數據庫ID(「相同行」概念)時,依賴某些字段(業務鍵)的相等性(用於未保存的對象)。對於Junit斷言,您可能希望所有字段均等。


所以我結束了創建,通過一個圖形對象運行,做他們的工作,因爲他們去。

通常有一個超類爬行對象:

  • 爬行穿過對象的所有屬性;停在:

    • 枚舉,
    • 框架類(如適用),
    • 在卸載代理或遙遠的連接,
    • 在對象已經訪問(避免循環)
    • 在多對多 - 一種關係,如果它們表示父母(通常不包括在等同語義中)
    • ...
  • 配置,以便它可以在某一時刻停止(完全停止,或者停止當前屬性裏面爬行):

    • 時mustStopCurrent()或mustStopCompletely()方法返回true,
    • 上遇到了一些註釋時, getter或一類,
    • 當電流(類,吸氣)屬於例外
    • 列表...

從這個爬行父,子類對於很多需要做:

  • 有關創建調試字符串(根據需要,有特殊情況的集合和數組沒有一個很好的toString調用toString;處理大小限制等等)。
  • 用於創建幾個均衡器(如前所述,對於使用ID的實體,對於所有字段或僅基於等於;)。這些均衡器通常也需要特殊情況(例如,對於您不能控制的類)。

回到一個問題:這些均衡器可以記得路徑不同的值,那將是非常有用的JUnit的情況下理解上的差異。

  • 爲了創建Orderers。例如,需要完成的保存實體是一個特定的順序,效率將決定將相同的類保存在一起將會大大提高。
  • 用於收集可在圖表中的各個級別找到的一組對象。然後循環使用收集器的結果非常簡單。

作爲補充,我必須說的是,除了實體在性能是一個真正的問題,我沒有選擇技術來實現的toString(),hashCode()方法,equals()和的compareTo()在我的實體上。例如,如果通過類的@UniqueConstraint在Hibernate中定義了一個或多個字段上的業務關鍵字,那麼讓我們假裝我的所有實體都具有在公共超類中實現的getIdent()屬性。 我的實體超有這4種方法依賴於這方面的知識,例如(空值需要被照顧的)的默認實現:

  • 的toString()打印「MyClass的(鍵1 =值,鍵2 =值)」
  • hashCode()方法是 「value1.hashCode()^ value2.hashCode()」
  • equals()方法是 「value1.equals(other.value1)& & value2.equals(other.value2)」
  • 的compareTo()是結合類,值1和value2的比較。

對於需要關注性能的實體,我只是簡單地覆蓋這些方法以不使用反射。我可以在迴歸JUnit測試中測試兩個實現的行爲是否相同。

1

單元測試應該明確定義,事他們測試。這意味着,你到底應該有明確定義的,東西,可那兩個對象的不同。如果有太多可以不同的東西,我會建議把這個測試分成幾個較小的測試。

+3

我不同意,這並不總是實際。例如說,我創建一個JPA實體,堅持它,然後檢索它,並且我想測試檢索到的對象是否與我存儲的對象相同。我只能爲頂級對象做這件事。 – skaffman 2009-09-11 15:45:18

+3

重點是他檢查2個對象是相同的(一個比較)。這些對象可能是複雜的,雖然斷言是微不足道的,但是當它們不同時對問題的診斷不是。 – 2009-09-11 15:51:29

+3

他的唯一的東西是「這些對象是否相等」 – 2009-09-11 18:22:21

0

我們使用一個叫做JUnitX的測試對我們所有的「共同」對象的平等契約庫: http://www.extreme-java.de/junitx/

我能想到的測試平等的不同部分的唯一方法()方法是將信息分解成更細粒度的內容。如果你正在測試一個深度嵌套的對象樹,你所做的並不是真正的單元測試。您需要使用該類型對象的單獨測試用例來測試圖中每個單獨對象上的equals()約定。您可以使用簡單的equals()實現的存根對象用於被測對象上的類類型字段。

HTH

0

我不會用toString(),因爲就像你說的,它通常用於顯示或記錄目的創建對象的一個​​很好的表現更有用。

聽起來你的「單元」測試不是隔離被測單元。例如,如果您的對象圖是A-->B-->C而您正在測試A,則您的A的單元測試不應該在意C中的equals()方法正在工作。你的單元測試C將確保它的工作。

因此,我將測試在測試以下爲Aequals()方法: - 比較兩個具有相同B甲對象的,在兩個方向,例如a1.equals(a2)a2.equals(a1)。 - 比較兩個方向

通過做這種方式,用一個JUnit斷言每次比較具有不同B的兩大A對象,你就知道該失敗的。

很明顯,如果你的班有更多的孩子是決定平等的一部分,你需要測試更多的組合。我想知道的是,你的單元測試不應該關心超出它直接接觸的類的行爲。在我的例子中,這意味着,你會假設C.equals()正常工作。

如果您正在比較集合,可能會出現一個摺痕。在這種情況下,我會使用一個實用程序來比較收藏,例如commons-collections CollectionUtils.isEqualCollection()。當然,只適用於被測單位的收藏品。

10

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} 
+1

+1感謝您的鏈接 – skaffman 2009-09-11 16:00:52

4

,因爲我往往設計複雜對象的方式,我有一個非常簡單的解決方案在這裏。

當我設計一個需要編寫equals方法(因此也是一個hashCode方法)的複雜對象時,我傾向於編寫一個字符串渲染器,並使用String類equals和hashCode方法。

渲染器當然不是必須的:它不一定非常容易讓人閱讀,並且包含所有和唯一需要比較的值,而且我習慣性地將它們按照控制我希望他們排序的方式;這些都不一定是真正的toString方法。

當然,我緩存這個呈現的字符串(以及hashCode的值)。它通常是私有的,但是將緩存的字符串package-private留給你可以讓你從單元測試中看到它。順便說一下,這並不總是我在交付系統中最終得到的 - 當然,如果性能測試顯示這種方法太慢,我準備替換它,但這是一種罕見的情況。到目前爲止,它只發生過一次,在一個系統中,可變對象正在迅速變化並且經常被比較。

我這樣做的原因是writing a good hashCode isn't trivial,並要求測試(*),同時使用字符串中的一個來避免測試。 (*考慮到Josh Bloch編寫一個好的hashCode方法的步驟3是測試它以確保「equal」對象具有相同的hashCode值,並確保涵蓋了所有可能的變體本身並不是小事。更微妙的,甚至難以考好是分佈)

+0

如何處理不保證子元素順序的結構。例如,地圖或集合。 – 2017-01-06 12:43:26

+1

@EldarBudagov請記住,我在2009年寫了我的答案,並且現在可能有更好的解決方案(xml,deepcopy等)。但我的方法是強制條目順序(通過使用鍵的自然順序,例如地圖),記住緩存字符串化版本。 還請記住,此方法僅適用於阻止修改或完全控制其子對象的對象,允許頂級對象在添加或刪除條目時知曉。 – CPerkins 2017-01-06 21:10:16

3

此問題的代碼存在於http://code.google.com/p/deep-equals/

使用DeepEquals.deepEquals(A,b)來比較兩個Java對象的語義平等。這將比較使用任何自定義equals()方法的對象(如果它們具有除Object.equals()之外的其他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()具有循環檢測,因此可用於任何對象圖。

+0

是否有可能看到對象不匹配的情況下有什麼區別?我也可以從比較中排除某些領域嗎? – tuk 2018-02-25 07:45:02

0

如果你願意用scala寫你的測試,你可以使用matchete。它的匹配的集合,可以使用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' 

正如你可以看到這裏,我一直在使用案例類,這些類被matchete識別,以便深入到對象圖中。 這是通過一個稱爲Diffable的類型類完成的。我不打算在這裏討論類型類,所以我們假設它是這個機制的基石,它比較給定類型的兩個實例。不是大小寫類型的類型(基本上所有類型都是Java)得到默認的Diffable,它使用equals。這是不是非常有用,除非您對特定類型提供Diffable

// 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代碼庫工作得很好。事實上,我一直在使用matchete來處理大型Java項目的上一份工作。

免責聲明:我是matchete作者:)