簡體   English   中英

使用反射來進行單元測試的復雜對象斷言是不好的做法嗎?

[英]Is it bad practice to use reflection to do complex object assertion for a unit test?

我正在閱讀這個主題 ,這是關於使用反射來測試私有變量...

但我的單元測試中沒有這樣的問題,我的代碼完全可以測試。

唯一的問題是我想通了,對具有預期結果的復雜對象的每個屬性進行斷言時非常耗時; 特別是對於復雜對象的列表。

由於它是一個復雜的對象,除非我為每個對象實現IEquality ,否則執行正常的Assert.AreEqual不會給我一個正確的結果。

但即使我這樣做,也不會告訴我在斷言期間哪個屬性/字段的名稱,期望值和實際值。

正確地說,我們手動將每個屬性值放入一個列表並執行單個CollectionAssertion ,但這仍然非常耗時,並且當斷言發生時它只告訴我元素值的索引不相等; 它不會告訴我屬性名稱。 這使得調試變得非常困難(我必須進入調試模式並查看集合中的元素)。

所以我想知道,如果我編寫一個遞歸反射方法,它將對兩個復雜對象進行斷言,它將告訴我每個屬性名稱,期望值,實際值。

這是一種好的做法還是不好的做法?

我發現很多人甚至不會考慮反思,但它有它的位置。 它在性能,類型安全等方面肯定存在缺陷,正如其他海報所述,但我認為單元測試是一個很好的使用場所。 只要它成功完成。

當您不擁有在屬性中使用的所有類型時,嘗試在所有對象上強制執行等同實現會進入牆。 實現一百個迷你比較器類與手動寫出斷言一樣耗時。

在過去,我編寫了一個擴展方法,可以執行您所描述的操作:

  • 比較兩個相同類型的對象(或實現一個通用接口)
  • 反射用於查找所有公共屬性。
  • 如果屬性是值類型,則完成Assert.AreEquals
  • 對於引用類型,它執行遞歸調用

我的測試在任何時候都不關心屬性名稱,因此重構重構並不重要。 實際上,會自動找到新屬性,而忘記刪除的屬性。

我從來沒有將它用於非常復雜的物體,但是它與我擁有的物體配合得很好,而不會減慢我的測試速度。

所以我認為在單元測試中可以隨意使用Reflection。

編輯:我會嘗試為你挖掘我的方法。

在我看來,使用Reflection不是一個很好的選擇。 使用Reflection意味着我們在編譯時失去了類型安全性。 而且,在使用Reflection之后,可能(通常)通過程序集的元數據進行不區分大小寫的字符串搜索。 這導致性能下降。 考慮到這些方面,我認為拆分原始類型(如oleksii所推薦)是一種很好的方法。

另一種方法可以是使用純訪問器方法編寫單獨的測試,以測試單獨的屬性集。 這可能不適用於所有情況。 但是,在某些情況下確實如此。

例如:如果我有一個Customer類,我可以編寫一個測試來檢查Address-type字段; 我可以編寫另一個測試來檢查訂單類型字段等等。

正常情況下,您不應該需要反射來做與測試相關的任何事情 在回答您鏈接的問題時提到了這一點:

反思應該只是最后的手段

如果需要檢查復雜對象是否相等, 請在單元測試中實現此類等式檢查 純粹用於單元測試目的的額外代碼沒有錯:

public void ComplexObjectsAreEqual()
{
    var first = // ...
    var second = // ...

    AssertComplexObjectsAreEqual(first, second);
}

private void AssertComplexObjectsAreEqual(ComplexObject first,
    ComplexObject second)
{
    Assert.That(first.Property1, Is.EqualTo(second.Property1),
       "Property1 differs: {0} vs {1}", first.Property1, second.Property1); 
    // ...
}

您不應該將單元測試視為其他代碼 如果需要編寫某些東西以使它們更具可讀性,清潔,可維護 - 寫下來。 它與其他地方的代碼相同。 您是否會通過生產代碼中的反射來比較對象?

我想說使用反射進行簡單的單元測試有很多正當理由。 引用https://github.com/kbilsted/StatePrinter

手動單元測試的問題

這很費勁。

當我一遍又一遍地打字並重新輸入時:Assert.This,Assert。那,......不禁想知道為什么計算機無法為我自動化這些東西。 所有那些不必要的打字需要時間並耗盡我的精力。

使用Stateprinter時,只要預期值和實際值不匹配,就會為您生成斷言。

代碼和測試不同步

當代碼更改時,例如通過向類添加字段,您需要在某些測試中添加斷言。 但是,找到一個完全手動的過程。 在沒有人對所有類進行完整概述的較大項目中,所需的更改不會在所有應該執行的地方執行。

將代碼從一個分支合並到另一個分支時會出現類似的情況。 假設您將發布分支中的錯誤修復或功能合並到開發分支,我一遍又一遍地觀察到代碼被合並,所有測試都運行然后提交合並。 人們忘記重新訪問並仔細檢查整個測試套件,以確定開發分支上是否存在測試,而不是合並發生的分支上的測試,相應地調整這些測試。

使用Stateprinter時,會比較對象圖而不是單個字段。 因此,當創建新字段時,所有相關測試都會失敗。 您可以將打印調整到特定字段,但是您無法自動檢測圖表中的更改。

可讀性差我

通過對測試類,測試方法和測試元素的標准命名的良好命名,您可以獲得很長的成功。 但是,沒有命名約定可以彌補斷言創建的視覺混亂。 當索引用於從列表或詞典中挑選元素時,會添加進一步的混亂。 並且在將它與for,foreach循環或LINQ表達式結合使用時,不要讓我開始。

使用StatePrinter時,會比較對象圖而不是單個字段。 因此,測試中不需要邏輯來挑選數據。

可讀性差二

當我讀下面的測試時。 想想這里真正重要的是什么

Assert.IsNotNull(result, "result");
Assert.IsNotNull(result.VersionData, "Version data");
CollectionAssert.IsNotEmpty(result.VersionData)
var adjustmentAccountsInfoData = result.VersionData[0].AdjustmentAccountsInfo;
Assert.IsFalse(adjustmentAccountsInfoData.IsContractAssociatedWithAScheme);
Assert.AreEqual(RiskGroupStatus.High, adjustmentAccountsInfoData.Status);
Assert.That(adjustmentAccountsInfoData.RiskGroupModel, Is.EqualTo(RiskGroupModel.Flexible));
Assert.AreEqual("b", adjustmentAccountsInfoData.PriceModel);
Assert.IsTrue(adjustmentAccountsInfoData.IsManual);

什么時候蒸餾我們想要表達的是什么

adjustmentAccountsInfoData.IsContractAssociatedWithAScheme = false
adjustmentAccountsInfoData.Status = RiskGroupStatus.High
adjustmentAccountsInfoData.RiskGroupModel = RiskGroupModel.Flexible
adjustmentAccountsInfoData.PriceModel = "b"
adjustmentAccountsInfoData.IsManual = true

可憐的可憐

當業務對象的字段數量增加時,相反的情況對於測試的可靠性來說也是如此。 是否覆蓋了所有領域? 字段是否被錯誤地多次比較? 還是反對錯誤的領域? 當你必須在一個對象上做25個斷言時,你就會知道痛苦,並且精心確保在正確的字段中檢查正確的字段。 然后審稿人必須經歷相同的練習。 為什么這不是自動化的?

使用StatePrinter時,會比較對象圖而不是單個字段。 您知道所有字段都已覆蓋,因為所有字段都已打印。

恕我直言,這是一個不好的做法,因為:

  • 反射代碼很慢,很難正確編寫
  • 維護起來更加困難,而且這些代碼可能不是重構友好的
  • 反射很慢,單元測試應該很快
  • 它也感覺不對勁

對我來說,這看起來好像是在試圖堵塞一個洞,而不是解決問題。 為了解決這個問題,我可以建議將一個大而復雜的類分成一組較小的類。 如果您有許多屬性 - 將它們分組到單個類中


這樣的課

class Foo
{
    T1 Prop1 {get; set;}
    T2 Prop2 {get; set;}
    T3 Prop3 {get; set;}
    T4 Prop4 {get; set;}
}

會成為

class Foo
{
    T12 Prop12 {get; set;}  
    T34 Prop34 {get; set;}  
}
class T12
{
    T1 Prop1 {get; set;}
    T2 Prop2 {get; set;}
}
class T34
{
    T3 Prop3 {get; set;}
    T4 Prop4 {get; set;}
}

注意, Foo現在只有一個屬性(即“分組”表示)。 如果您可以以某種方式對屬性進行分組,那么任何狀態更改都將本地化為特定組 - 您的任務將變得更加簡化。 然后,您可以斷言“分組”屬性等於預期狀態。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM