2012-11-21 57 views
9

現在的短外賣該解決方案已被發現:凍結模擬就好AutoFixture/AutoMoq忽略注入實例/冷凍模擬

AutoFixture的回報;我的這個AutoFixture也產生了一個公共屬性,它有一個對測試非常重要的本地默認屬性,並且AutoFixture設置了一個新的值。除了Mark的答案之外,還有很多東西需要學習。

原題:

我開始嘗試AutoFixture昨天有起訂量遍他們我xUnit.net測試。我希望能夠替換Moq中的一些內容或使其更易於閱讀,而且我特別感興趣的是在SUT Factory容量中使用AutoFixture。

我用自己的一些關於AutoMocking的Mark Seemann的博客文章武裝自己,試圖從那裏工作,但我沒有走得很遠。

這是我的測試中看起來就像沒有AutoFixture:

[Fact] 
public void GetXml_ReturnsCorrectXElement() 
{ 
    // Arrange 
    string xmlString = @" 
     <mappings> 
      <mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' /> 
      <mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' /> 
     </mappings>"; 

    string settingKey = "gcCreditApplicationUsdFieldMappings"; 

    Mock<ISettings> settingsMock = new Mock<ISettings>(); 
    settingsMock.Setup(s => s.Get(settingKey)).Returns(xmlString); 
    ISettings settings = settingsMock.Object; 

    ITracingService tracing = new Mock<ITracingService>().Object; 

    XElement expectedXml = XElement.Parse(xmlString); 

    IMappingXml sut = new SettingMappingXml(settings, tracing); 

    // Act 
    XElement actualXml = sut.GetXml(); 

    // Assert 
    Assert.True(XNode.DeepEquals(expectedXml, actualXml)); 
} 

這裏的故事很簡單 - 確保SettingMappingXml查詢使用正確的密鑰的ISettings依賴(這是硬編碼/屬性注入)和返回結果爲XElement。只有在出現錯誤時,ITracingService纔有意義。

我想要做的是擺脫明確創建ITracingService對象的需要,然後手動注入依賴關係(不是因爲此測試太複雜,而是因爲它足夠簡單,可以嘗試並理解它們)。

輸入AutoFixture - 第一次嘗試:

[Fact] 
public void GetXml_ReturnsCorrectXElement() 
{ 
    // Arrange 
    IFixture fixture = new Fixture(); 
    fixture.Customize(new AutoMoqCustomization()); 

    string xmlString = @" 
     <mappings> 
      <mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' /> 
      <mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' /> 
     </mappings>"; 

    string settingKey = "gcCreditApplicationUsdFieldMappings"; 

    Mock<ISettings> settingsMock = new Mock<ISettings>(); 
    settingsMock.Setup(s => s.Get(settingKey)).Returns(xmlString); 
    ISettings settings = settingsMock.Object; 
    fixture.Inject(settings); 

    XElement expectedXml = XElement.Parse(xmlString); 

    IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>(); 

    // Act 
    XElement actualXml = sut.GetXml(); 

    // Assert 
    Assert.True(XNode.DeepEquals(expectedXml, actualXml)); 
} 

我希望CreateAnonymous<SettingMappingXml>(),一旦檢測到ISettings構造函數的參數,要注意到一個具體的實例已經被註冊了該接口並注入是 - 然而,它不不這樣做,而是創建一個新的匿名實現。

,這一點尤其令人混淆,因爲fixture.CreateAnonymous<ISettings>()確實返回我的實例 -

IMappingXml sut = new SettingMappingXml(fixture.CreateAnonymous<ISettings>(), fixture.CreateAnonymous<ITracingService>()); 

使得測試完全綠色,此行正是我所預料AutoFixture在內部做實例SettingMappingXml時。

再就是凍結組件的概念,所以我繼續只是凍結在夾具模擬,而不是領的嘲笑對象:

fixture.Freeze<Mock<ISettings>>(f => f.Do(m => m.Setup(s => s.Get(settingKey)).Returns(xmlString))); 

果然這工作完全正常 - 只要我打電話SettingMappingXml構造函數明確並且不依賴於CreateAnonymous()



簡單地說,我不明白爲什麼它的工作原理是顯然的方式做,因爲它違背了我能變出任何邏輯。 通常我會懷疑庫中存在一個bug,但這是一件非常基本的事情,我相信其他人會遇到這種情況,並且很久以前就會發現並修復它。更重要的是,瞭解Mark對測試和DI的孜孜以求,這不是無意的。

這反過來意味着我必須失去一些東西而不是基本的東西。我怎樣才能讓我的SUT通過AutoFixture創建一個預配置的模擬對象作爲依賴項?我現在唯一確定的是我需要AutoMoqCustomization,所以我不必爲ITracingService配置任何東西。

AutoFixture/AutoMoq包是2.14.1,Moq是3.1.416.3,全部來自NuGet。 .NET版本是4.5(與VS2012安裝),行爲是在VS2012相同,2010年

雖然寫這篇文章,我發現有些人具有起訂量4.0問題和程序集綁定重定向,所以我精心清洗我的通過將AutoFixture.AutoMoq安裝到「乾淨」項目中來安裝Moq 4的任何實例並安裝了Moq 3.1。但是,我的測試行爲保持不變。

謝謝你的指點和解釋。

更新:這裏的構造函數代碼馬克問:

public SettingMappingXml(ISettings settingSource, ITracingService tracing) 
{ 
    this._settingSource = settingSource; 
    this._tracing = tracing; 

    this.SettingKey = "gcCreditApplicationUsdFieldMappings"; 
} 

而對於完整性,該GetXml()方法是這樣的:

public XElement GetXml() 
{ 
    int errorCode = 10600; 

    try 
    { 
     string mappingSetting = this._settingSource.Get(this.SettingKey); 
     errorCode++; 

     XElement mappingXml = XElement.Parse(mappingSetting); 
     errorCode++; 

     return mappingXml; 
    } 
    catch (Exception e) 
    { 
     this._tracing.Trace(errorCode, e.Message); 
     throw; 
    } 
} 

SettingKey僅僅是一個自動財產。

+0

我無法重現你報什麼。上述測試(使用AutoFixture)在我的機器上成功。 SettingMappingXml構造函數的外觀如何? –

回答

13

假設SettingKey屬性定義如下,我現在可以重現該問題:

public string SettingKey { get; set; } 

什麼情況是,注入SettingMappingXml實例Test Doubles是完全正常的,但因爲SettingKey是可寫的, AutoFixture的自動屬性功能可以啓動並修改值。

考慮以下代碼:

var fixture = new Fixture().Customize(new AutoMoqCustomization()); 
var sut = fixture.CreateAnonymous<SettingMappingXml>(); 
Console.WriteLine(sut.SettingKey); 

這將打印像這樣:

SettingKey83b75965-2886-4308-bcc4-eb0f8e63de09

即使所有的測試雙打是正確的注入,Setup方法的期望不符合。

有很多方法可以解決這個問題。

保護不變

解決這個問題的正確方法是使用單元測試和AutoFixture作爲一種反饋機制。這是GOOS中的一個關鍵點:單元測試的問題通常是關於設計缺陷的症狀,而不是單元測試(或AutoFixture)本身的缺點。

在這種情況下,它向我表明the design isn't fool-proof enough。客戶可以隨意操縱SettingKey真的合適嗎?

作爲最低限度,我會建議替代實現這樣的:

public string SettingKey { get; private set; } 

隨着這種變化,我的攝製通行證。

省略SettingKey

如果你不能(或不願)改變你的設計,你可以指示AutoFixture跳過設置SettingKey屬性:

IMappingXml sut = fixture 
    .Build<SettingMappingXml>() 
    .Without(s => s.SettingKey) 
    .CreateAnonymous(); 

就個人而言,我覺得它每次需要某個特定類的實例時,都必須編寫Build表達式,這種情況適得其反。您可以斷開如何SettingMappingXml實例從實際實例創建:

fixture.Customize<SettingMappingXml>(
    c => c.Without(s => s.SettingKey)); 
IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>(); 

藉此進一步,可以封裝了Customize方法調用中a Customization

public class SettingMappingXmlCustomization : ICustomization 
{ 
    public void Customize(IFixture fixture) 
    { 
     fixture.Customize<SettingMappingXml>(
      c => c.Without(s => s.SettingKey)); 
    } 
} 

這需要你與自定義創建Fixture實例:

IFixture fixture = new Fixture() 
    .Customize(new SettingMappingXmlCustomization()) 
    .Customize(new AutoMoqCustomization()); 

一旦你超過兩個或三個自定義,鏈越多,你可能會感到厭倦編寫方法鏈中的所有時間。現在是時候來封裝這些自定義成一組約定爲您的特定庫:

public class TestConventions : CompositeCustomization 
{ 
    public TestConventions() 
     : base(
      new SettingMappingXmlCustomization(), 
      new AutoMoqCustomization()) 
    { 
    } 
} 

這使您可以隨時創建Fixture情況是這樣的:

IFixture fixture = new Fixture().Customize(new TestConventions()); 

TestConventions爲您提供了一個集中的地方,您可以在需要時偶爾修改測試套件的約定。它減少了單元測試的可維護性稅,並有助於保持生產代碼的設計更加一致。

最後,由於看起來好像您使用的是xUnit.net,您可以使用AutoFixture's xUnit.net integration,但在此之前,您需要使用不太必要的操作方式來操作Fixture。事實證明,它創建,配置和注入的ISettings測試替身的代碼是如此地道,它有一個名爲Freeze快捷:

fixture.Freeze<Mock<ISettings>>() 
    .Setup(s => s.Get(settingKey)).Returns(xmlString); 

有了到位,下一步就是定義一個定製AutoDataAttribute:

public class AutoConventionDataAttribute : AutoDataAttribute 
{ 
    public AutoConventionDataAttribute() 
     : base(new Fixture().Customize(new TestConventions())) 
    { 
    } 
} 

現在,您可以測試減少到最基本的要素,擺脫所有的噪音,使測試簡明扼要地表達只有哪些事項:

[Theory, AutoConventionData] 
public void ReducedTheory(
    [Frozen]Mock<ISettings> settingsStub, 
    SettingMappingXml sut) 
{ 
    string xmlString = @" 
     <mappings> 
      <mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' /> 
      <mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' /> 
     </mappings>"; 
    string settingKey = "gcCreditApplicationUsdFieldMappings"; 
    settingsStub.Setup(s => s.Get(settingKey)).Returns(xmlString); 

    XElement actualXml = sut.GetXml(); 

    XElement expectedXml = XElement.Parse(xmlString); 
    Assert.True(XNode.DeepEquals(expectedXml, actualXml)); 
} 

其他選項

爲了使原來的測試通不過,你也可以只關閉自動性質完全:

fixture.OmitAutoProperties = true; 
+2

謝謝你這個非常透徹的答案。所以我在回答Nikos時至少是正確的 - 一個不明顯的錯誤,但是愚蠢。公共屬性的目的是允許我在某個時候使用具有不同值的類,儘管現在我只需要在構造函數中設置的默認值。在拐角處和在腳下射擊自己的路上...... – TeaDrivenDev

4

在第一測試可以創建應用Fixture類與AutoMoqCustomization的實例:

var fixture = new Fixture() 
    .Customize(new AutoMoqCustomization()); 

然後,唯一變化是:

步驟1

// The following line: 
Mock<ISettings> settingsMock = new Mock<ISettings>(); 
// Becomes: 
Mock<ISettings> settingsMock = fixture.Freeze<Mock<ISettings>>(); 

第2步

// The following line: 
ITracingService tracing = new Mock<ITracingService>().Object; 
// Becomes: 
ITracingService tracing = fixture.Freeze<Mock<ITracingService>>().Object; 

步驟3

// The following line: 
IMappingXml sut = new SettingMappingXml(settings, tracing); 
// Becomes: 
IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>(); 

這就是它!


這裏是它如何工作的:

內部,Freeze創建請求的類型(例如Mock<ITracingService>)的一個實例,然後注入它所以當你再次請求它,它總是會返回該實例。

這就是我們在Step 1Step 2中所做的。

Step 3我們要求SettingMappingXml類型取決於ISettingsITracingService的一個實例。由於我們使用自動模擬,因此Fixture類將爲這些接口提供模擬。不過,我們以前注入他們與Freeze,所以已經創建的嘲笑現在自動提供。

+0

感謝您的回答。第2步是我根本不需要做的 - 我不關心ITracingService實例,只想讓AutoFixture來處理它。至於'ISetting'實例 - 'CreateAnonymous ()'提供了一個,但不是我凍結的那個。 'CreateAnonymous ()'雖然實際上會返回凍結的實例。隨着馬克和你的回答,我開始覺得必須有一些地方我做了一些不明顯的事情,但很笨拙。 – TeaDrivenDev

+0

您可以跳過第2步,然後由'Fixture'類提供自動模擬的實例。從構造函數的源代碼中,在執行第3步之後,你應該得到一個'SettingMappingXml'實例和* frozen *'ISetting'嘲諷實例。 –