2011-04-18 23 views
6

如果稍後驗證方法是否被調用,在回調中進行斷言是否可接受?這是確保我的模擬獲得傳遞給它的預期參數的首選方法,還是應該在回調中設置局部變量並在該實例上執行斷言?驗證傳遞給模擬參數的正確方法是按預期方式設置

我有一種情況,我在Presenter類中有一些邏輯,它根據輸入派生值並將它們傳遞給Creator類。爲了測試Presenter類中的邏輯,我想驗證在創建者被調用時觀察到適當的派生值。我想出了下面的作品的例子,但我不知道我是否喜歡這種方法:

[TestFixture] 
public class WidgetCreatorPresenterTester 
{ 
    [Test] 
    public void Properly_Generates_DerivedName() 
    { 
     var widgetCreator = new Mock<IWidgetCreator>(); 
     widgetCreator.Setup(a => a.Create(It.IsAny<Widget>())) 
        .Callback((Widget widget) => 
        Assert.AreEqual("Derived.Name", widget.DerivedName)); 

     var presenter = new WidgetCreatorPresenter(widgetCreator.Object); 
     presenter.Save("Name"); 

     widgetCreator.Verify(a => a.Create(It.IsAny<Widget>()), Times.Once()); 
    } 
} 

我很擔心,因爲沒有在年底Verify電話,也不能保證斷言回調將被調用。另一種方法是設置一個局部變量的回調:

[Test] 
public void Properly_Generates_DerivedName() 
{ 
    var widgetCreator = new Mock<IWidgetCreator>(); 
    Widget localWidget = null; 
    widgetCreator.Setup(a => a.Create(It.IsAny<Widget>())) 
     .Callback((Widget widget) => localWidget = widget); 

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object); 
    presenter.Save("Name"); 

    widgetCreator.Verify(a => a.Create(It.IsAny<Widget>()), Times.Once()); 
    Assert.IsNotNull(localWidget); 
    Assert.AreEqual("Derived.Name", localWidget.DerivedName); 
} 

我覺得這種做法是不容易出錯,因爲它是更加明確,而且更容易地看到,Assert語句將被調用。一種方法比另一種更好嗎?有沒有更簡單的方法來測試傳遞給我缺少的模擬的輸入參數?

如果它是有幫助的,這裏是代碼的這個例子的其餘部分:

public class Widget 
{ 
    public string Name { get; set; } 
    public string DerivedName { get; set; } 
} 

public class WidgetCreatorPresenter 
{ 
    private readonly IWidgetCreator _creator; 

    public WidgetCreatorPresenter(IWidgetCreator creator) 
    { 
     _creator = creator; 
    } 

    public void Save(string name) 
    { 
     _creator.Create(
      new Widget { Name = name, DerivedName = GetDerivedName(name) }); 
    } 

    //This is the method I want to test 
    private static string GetDerivedName(string name) 
    { 
     return string.Format("Derived.{0}", name); 
    } 
} 

public interface IWidgetCreator 
{ 
    void Create(Widget widget); 
} 

編輯
我更新的代碼,使我的問題概括更容易使用第二種方法。我將Setup/Verify中使用的表達式創建爲一個單獨的變量,所以我只需要定義一次。我覺得這個方法是我最熟悉的,它很容易設置,並且出現錯誤信息時失敗。

[Test] 
public void Properly_Generates_DerivedName() 
{ 
    var widgetCreator = new Mock<IWidgetCreator>(); 
    Widget localWidget = null; 

    Expression<Action<IWidgetCreator>> expressionCreate = 
     (w => w.Create(It.IsAny<Widget>())); 
    widgetCreator.Setup(expressionCreate) 
     .Callback((Widget widget) => localWidget = widget); 

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object); 
    presenter.Save("Name"); 

    widgetCreator.Verify(expressionCreate, Times.Once()); 
    Assert.IsNotNull(localWidget); 
    Assert.AreEqual("Derived.Name", localWidget.DerivedName); 
} 

回答

3

因爲你的代碼的結構方式,你有種被迫在一個單元測試,測試兩件事情。您正在測試A)您的演示者正在調用注入的WidgetCreator的create方法,並且B)在新的Widget上設置了正確的名稱。如果可能的話,如果你能以某種方式將這兩件事情做成兩個單獨的測試,會更好,但在這種情況下,我真的沒有辦法做到這一點。

鑑於這一切,我認爲第二種方法更清潔。對於你的期望,它更加明確,如果失敗了,它會完全理解失敗的原因和方式。

+0

我同意第二種方法更可取。我喜歡@ aqwert的使用驗證的建議,但失敗消息太難以處理。我在原始問題中添加了一些代碼來更新第二種方法,因此我只需要聲明一次表達式。想想我會堅持這種做法。 – rsbarro 2011-04-23 04:36:43

4

我所做的是做Verify匹配符合AAA。因爲這個安裝程序並不是必需的。你可以內聯它,但我把它分開,使它看起來更乾淨。

[Test] 
public void Properly_Generates_DerivedName() 
{ 
    var widgetCreator = new Mock<IWidgetCreator>(); 

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object); 
    presenter.Save("Name"); 

    widgetCreator.Verify(a => a.Create(MatchesWidget("Derived.Name")); 
} 

private Widget MatchesWidget(string derivedName) 
{ 
    return It.Is<Widget>(m => m.DerivedName == derivedName); 
} 
+0

+1感謝您的回答。比我的例子更容易閱讀,但單元測試的失敗信息有點奇怪。而不是「預期,但是」消息,你會得到「期望的模擬調用至少一次,但從未執行過」。一旦習慣了它,處理起來並不難,但如果你不習慣看到這種類型的信息,可能會有點不清楚。 – rsbarro 2011-04-18 23:15:30

+1

是的,嘲笑框架消息可能有點奇怪。你可以添加一個失敗消息到'Verify'方法,如果這有幫助 – aqwert 2011-04-18 23:19:15

+0

是的,失敗消息確實有幫助。我將打開這個問題,看看是否有其他建議。再次感謝。 – rsbarro 2011-04-18 23:44:09

3

只是爲了闡述@ rsbarro的評論 - 起訂量失敗的錯誤消息:

預計在模擬調用至少一次,但從未進行

...小於有用對於複雜類型,在確定which條件時確實發生故障時,查找錯誤時(無論是在代碼或單元測試中)。

我使用起訂量Verify到驗證大量的在Verify的條件下,其中,該方法必須已經調用與特定的參數值,其不象intstring原語時經常遇到這種情況。 (對於原始類型,這通常不是一個問題,因爲Moq列出了方法中實際的「已執行調用」作爲異常的一部分)。因此,在這種情況下,我需要捕獲傳入的參數(對於我來說,這似乎與Moq的工作重複),或者只需將內嵌的斷言與Setup/Callbacks內嵌即可。

例如覈查:

widgetCreator.Verify(wc => wc.Create(
     It.Is<Widget>(w => w.DerivedName == "Derived.Name" 
        && w.SomeOtherCondition == true), 
     It.Is<AnotherParam>(ap => ap.AnotherCondition == true), 
    Times.Exactly(1)); 

將被重新編碼爲

widgetCreator.Setup(wc => wc.Create(It.IsAny<Widget>(), 
            It.IsAny<AnotherParam>()) 
      .Callback<Widget, AnotherParam>(
       (w, ap) => 
       { 
        Assert.AreEqual("Derived.Name", w.DerivedName); 
        Assert.IsTrue(w.SomeOtherCondition); 
        Assert.IsTrue(ap.AnotherCondition, "Oops"); 
       }); 

// *** Act => invoking the method on the CUT goes here 

// Assert + Verify - cater for rsbarro's concern that the Callback might not have happened at all 
widgetCreator.Verify(wc => wc.Create(It.IsAny<Widget>(), It.Is<AnotherParam>()), 
    Times.Exactly(1)); 

乍一看,這違反AAA,因爲我們把Assert內嵌在Arrange(雖然回調只是在調用Act),但至少我們可以深入到問題的底部。

另請參見Hady的將跟蹤回調lambda移動到其自己的命名函數中的想法,或者更好的是,在C#7中,可以將它移動到單元測試方法底部的Local Function,所以AAA佈局可以保留。

1

建立在StuartLC的回答這個話題上,你可以通過編寫一個傳遞給模擬對象的Verify方法的「inline」函數來遵循他的建議,而不違反AAA

因此,例如:

// Arrange 
widgetCreator 
    .Setup(wc => wc.Create(It.IsAny<Widget>(), It.IsAny<AnotherParam>()); 

// Act 
// Invoke action under test here... 

// Assert 
Func<Widget, bool> AssertWidget = request => 
{ 
    Assert.AreEqual("Derived.Name", w.DerivedName); 
    Assert.IsTrue(w.SomeOtherCondition); 
    Assert.IsTrue(ap.AnotherCondition, "Oops"); 
    return true; 
}; 

widgetCreator 
    .Verify(wc => wc.Create(It.Is<Widget>(w => AssertWidget(w)), It.Is<AnotherParam>()), Times.Exactly(1)); 
相關問題