2010-09-09 82 views
6

我有一個集成測試LoadFile_DataLoaded_Successfully()。我想重構它到單元測試來打破與filesytem的依賴關係。與文件系統依賴關係的TDD

P.S.我在TDD是新:

這裏是我的製作等級:

public class LocalizationData 
{ 
    private bool IsValidFileName(string fileName) 
    { 
     if (fileName.ToLower().EndsWith("xml")) 
     { 
      return true; 
     } 
     return false; 
    } 

    public XmlDataProvider LoadFile(string fileName) 
    { 
     if (IsValidFileName(fileName)) 
     { 
      XmlDataProvider provider = 
          new XmlDataProvider 
           { 
             IsAsynchronous = false, 
             Source = new Uri(fileName, UriKind.Absolute) 
           }; 

      return provider; 
     } 
     return null; 
    } 
} 

和我的測試類(NUnit的)

[TestFixture] 
class LocalizationDataTest 
{ 
    [Test] 
    public void LoadFile_DataLoaded_Successfully() 
    { 
     var data = new LocalizationData(); 
     string fileName = "d:/azeri.xml"; 
     XmlDataProvider result = data.LoadFile(fileName); 
     Assert.IsNotNull(result); 
     Assert.That(result.Document, Is.Not.Null); 
    } 
} 

任何想法如何重構它打破文件系統依賴

+0

我認爲一些假冒的對象必須是使用替代XmlDataProvider – Polaris 2010-09-09 14:48:28

+0

您可能會更好上更爲複雜,easer代碼花費你的時間去測試。這樣的事情可能會被系統級別的測試所覆蓋。 – 2010-09-09 15:02:44

+0

不要忘記標記您最喜歡的答案;-) – Steven 2010-11-21 16:43:07

回答

2

在我的(Python)項目之一中,我假設所有單元測試都運行在包含文件夾「數據」(輸入文件)和「輸出」(輸出文件)的特殊目錄中。我使用的測試腳本首先檢查這些文件夾是否存在(即,如果當前工作目錄是正確的),然後運行測試。然後,我的單元測試可以使用相關文件名,如「data/test-input.txt」。

我不知道如何在C#中做到這一點,但也許你可以在測試SetUp方法中測試文件「data/azeri.xml」是否存在。

+1

這很有趣,但我認爲這種方法對C#應用程序不利。我認爲必須創建一些存根對象 – Polaris 2010-09-09 14:57:41

+1

調用文件系統的任何測試都不是*單元測試。 – 2010-09-09 15:40:19

+2

@Billy ONeal:只要輸入參數保持不變,任何測試單元(或模塊)都是單元測試IMO。如果輸入參數太大或模塊執行文件操作(例如用於管理臨時文件的模塊),則單元測試需要讀取或寫入文件。 – AndiDog 2010-09-09 16:41:43

0

在這種情況下,您基本上處於較低級別的依賴關係。您正在測試是否存在文件,並且可以使用該文件作爲源創建xmlprovider。

您可以打破依賴關係的唯一方法是注入一些東西來創建XmlDataProvider。你可以嘲笑它返回你創建的XmlDataProvider(而不是閱讀)。作爲簡單的例子將是:

class XmlDataProviderFactory 
{ 
    public virtual XmlDataProvider NewXmlDataProvider(string fileName) 
    { 
     return new XmlDataProvider 
        { 
         IsAsynchronous = false, 
         Source = new Uri(fileName, UriKind.Absolute) 
        }; 
} 

class XmlDataProviderFactoryMock : XmlDataProviderFactory 
{ 
    public override XmlDataProvider NewXmlDataProvider(string fileName) 
    { 
     return new XmlDataProvider(); 
    } 
} 

public class LocalizationData 
{ 
... 
    public XmlDataProvider LoadFile(string fileName, XmlDataProviderFactory factory) 
     { 
      if (IsValidFileName(fileName)) 
      { 
       return factory.NewXmlDataProvider(fileName); 
      } 
      return null; 
     } 
} 

[TestFixture] 
class LocalizationDataTest 
{ 
    [Test] 
    public void LoadFile_DataLoaded_Succefully() 
    { 
     var data = new LocalizationData(); 
     string fileName = "d:/azeri.xml"; 
     XmlDataProvider result = data.LoadFile(fileName, new XmlDataProviderFactoryMock()); 
     Assert.IsNotNull(result); 
     Assert.That(result.Document, Is.Not.Null); 
    } 

} 

使用注射框架可以通過注入工廠在類的構造或其他地方簡化呼叫到LoadFile

+0

我同意。如果你想單元測試這個,你必須從類的範圍之外提供XMLDataProvider,而不是新建一個。我認爲,在這種情況下,您不會因爲在這個範圍狹窄的集成測試中停止您的測試覆蓋粒度而被公開抨擊。創建模擬只是爲了驗證某個特定的調用是否已經完成,尤其是考慮到該類已經寫入了,這既不是必需的,也不是真正的TDD(測試將被寫入以驗證您已經編寫的代碼是如何工作的打算,不要驗證你還沒有寫入的代碼 – KeithS 2010-09-09 15:02:58

+0

@KeithS。我同意這是一個關於如何通過使用模擬對象來實現解耦的例子 – Rod 2010-09-09 15:07:01

1

爲什麼使用XmlDataProvider?就目前而言,我認爲這不是一個有價值的單元測試。相反,爲什麼你不測試你對數據提供者做什麼?

例如,如果您使用XML數據加載了Foo對象的列表,使接口:

public interface IFooLoader 
{ 
    IEnumerable<Foo> LoadFromFile(string fileName); 
} 

然後,您可以使用您在一個生成測試文件來測試你的這個類的實現單元測試。這樣你就可以打破你對文件系統的依賴。當您的測試退出時(在finally塊中)刪除文件。

而對於使用此類型的協作者,您可以傳入模擬版本。您可以手動編寫模擬代碼,也可以使用模擬框架,如Moq,Rhino,TypeMock或NMock。嘲笑是很好的,但如果你是TDD的新手,那麼在你瞭解他們的用途時,手動編寫你的模擬代碼是很好的。一旦你有了這些,那麼你就可以很好地理解嘲笑框架的好,壞和醜陋。當你開始使用TDD時,他們可能有點粗糙。你的旅費可能會改變。

祝你好運。

8

你在這裏失蹤的是控制反轉。舉例來說,你可以介紹依賴注入的原則,爲您的代碼:

public interface IXmlDataProviderFactory 
{ 
    XmlDataProvider Create(string fileName); 
} 
public class LocalizationData 
{ 
    private IXmlDataProviderFactory factory; 
    public LocalizationData(IXmlDataProviderFactory factory) 
    { 
     this.factory = factory; 
    } 

    private bool IsValidFileName(string fileName) 
    { 
     return fileName.ToLower().EndsWith("xml"); 
    } 

    public XmlDataProvider LoadFile(string fileName) 
    { 
     if (IsValidFileName(fileName)) 
     { 
      XmlDataProvider provider = this.factory.Create(fileName); 
      provider.IsAsynchronous = false; 
      return provider; 
     } 
     return null; 
    } 
} 

在創建XmlDataProvider上面的代碼抽離使用IXmlDataProviderFactory接口。該接口的實現可以在LocalizationData的構造函數中提供。現在,您可以編寫單元測試如下:

[Test] 
public void LoadFile_DataLoaded_Succefully() 
{ 
    // Arrange 
    var expectedProvider = new XmlDataProvider(); 
    string validFileName = CreateValidFileName(); 
    var data = CreateNewLocalizationData(expectedProvider); 

    // Act 
    var actualProvider = data.LoadFile(validFileName); 

    // Assert 
    Assert.AreEqual(expectedProvider, actualProvider); 
} 

private static LocalizationData CreateNewLocalizationData(
    XmlDataProvider expectedProvider) 
{ 
    return new LocalizationData(FakeXmlDataProviderFactory() 
    { 
     ProviderToReturn = expectedProvider 
    }); 
} 

private static string CreateValidFileName() 
{ 
    return "d:/azeri.xml"; 
} 

FakeXmlDataProviderFactory看起來是這樣的:

class FakeXmlDataProviderFactory : IXmlDataProviderFactory 
{ 
    public XmlDataProvider ProviderToReturn { get; set; } 

    public XmlDataProvider Create(string fileName) 
    { 
     return this.ProviderToReturn; 
    } 
} 

現在在測試環境中,你可以(並且可能應該)總是手動創建測試類。但是,您希望將工具方法中的創建抽象出來,以防止在被測試的類更改時不得不更改許多測試。

但是,在您的生產環境中,手動創建類時很快就會變得非常麻煩。特別是當它包含很多依賴關係時。這是IoC/DI框架的亮點。他們可以幫助你。舉例來說,當你想在你的產品代碼使用LocalizationData,你可能會這樣寫代碼:

var localizer = ServiceLocator.Current.GetInstance<LocalizationData>(); 

var data = data.LoadFile(fileName); 

請注意,我使用的是Common Service Locator來作爲例子。

該框架將負責爲您創建該實例。然而,使用這樣的依賴注入框架,您將不得不讓框架知道您的應用程序需要哪些「服務」。舉例來說,當我使用Simple Service Locator圖書館爲例(無恥插頭就是),你的配置可能是這樣的:

var container = new SimpleServiceLocator(); 

container.RegisterSingle<IXmlDataProviderFactory>(
    new ProductionXmlDataProviderFactory()); 

ServiceLocator.SetLocatorProvider(() => container); 

此代碼通常會在應用程序的啓動路徑。當然,唯一缺少的難題是實際的ProductionXmlDataProviderFactory班。下面是它:

class ProductionXmlDataProviderFactory : IXmlDataProviderFactory 
{ 
    public XmlDataProvider Create(string fileName) 
    { 
     return new XmlDataProvider 
     { 
      Source = new Uri(fileName, UriKind.Absolute) 
     }; 
    } 
} 

請也沒有,你可能不希望新的你LocalizationData在生產自己的代碼,因爲這個類可能是使用依賴於這種類型的其他類。你通常會做的是要求框架爲你創建最高級的類(例如實現完整用例的命令)並執行它。

我希望這會有所幫助。

+0

你可以引入你想要的所有工廠,但是如果你將這個參數作爲一個文件名來處理,那麼你就依賴於文件系統,我也曾經這樣做過,但我把它解釋爲一個虛假的抽象概念,有人會把它稱爲「漏洞」抽象從結構上說,你不依賴於文件系統,但是代碼仍然只能用於文件系統,因爲'fileName'並不意味着其他任何東西。 – 2012-05-31 18:34:33

0

我喜歡@史蒂芬的回答除了我認爲他做的還遠遠不夠:

public interface DataProvider 
{ 
    bool IsValidProvider(); 
    void DisableAsynchronousOperation(); 
} 

public class XmlDataProvider : DataProvider 
{ 
    private string fName; 
    private bool asynchronousOperation = true; 

    public XmlDataProvider(string fileName) 
    { 
     fName = fileName; 
    } 

    public bool IsValidProvider() 
    { 
     return fName.ToLower().EndsWith("xml"); 
    } 

    public void DisableAsynchronousOperation() 
    { 
     asynchronousOperation = false; 
    } 
} 


public class LocalizationData 
{ 
    private DataProvider dataProvider; 

    public LocalizationData(DataProvider provider) 
    { 
     dataProvider = provider; 
    } 

    public DataProvider Load() 
    { 
     if (provider.IsValidProvider()) 
     { 
      provider.DisableAsynchronousOperation(); 
      return provider; 
     } 
     return null; 
    } 
} 

通過不夠深入我的意思是,他沒有按照Last Possible Responsible Moment。儘可能多地推入實施的DataProvider課程。

有一件事我沒有用這個代碼做,就是用單元測試和模擬來驅動它。這就是爲什麼你仍在檢查提供商的狀態,看看它是否爲有效

另一件事是我試圖刪除使本地化數據知道提供者正在使用文件的依賴關係。如果它是一個Web服務或數據庫呢?

0

所以首先讓我們瞭解我們需要測試的東西。我們需要驗證給定一個有效的文件名,你的LoadFile(fn)方法返回一個XmlDataProvider,否則返回null。

爲什麼LoadFile()方法難以測試?因爲它創建了一個帶有從文件名創建的URI的XmlDataProvider。我對C#沒有太多的工作,但是假設如果文件實際上不存在於系統中,我們將得到一個異常。 真正的問題是,你的生產方法LoadFile()正在創造一些難以僞造的東西。不能僞造它是一個問題,因爲我們無法確保在所有測試環境中存在某個文件,而無需執行隱式指導。

所以解決方案是 - 我們應該能夠僞裝loadFile方法的協作者(XmlDataProvider)。然而,如果一種方法創建它的合作者,它不能僞造它們,因此一種方法不應該創建它的合作者。

如果一種方法沒有創建它的合作者,它是如何得到它們的? - 在這兩種方法之一:

  1. 他們應注入
  2. 他們應該從一些工廠

獲得在這種情況下,它沒有任何意義的XmlDataProvider是方法注入該方法,因爲這正是它返回的。所以我們應該從全局工廠 - XmlDataProviderFactory中獲取它。

這裏有趣的部分。當您的代碼在生產環境中運行時,工廠應該返回一個XmlDataProvider,並且當您的代碼在測試環境中運行時,工廠應該返回一個假對象。

現在唯一難解的部分是,如何確保工廠在不同環境中以不同方式運行?一種方法是使用一些在兩種環境中都具有不同值的屬性,另一種方法是配置工廠應該返回的值。我個人比較喜歡前一種方式。

希望這會有所幫助。

3

當我在下面的代碼看看:

public class LocalizationData 
{ 
    private static bool IsXML(string fileName) 
    { 
     return (fileName != null && fileName.ToLower().EndsWith("xml")); 
    } 

    public XmlDataProvider LoadFile(string fileName) 
    { 
     if (!IsXML(fileName)) return null*; 
     return new XmlDataProvider{ 
            IsAsynchronous = false, 
            Source = new Uri(fileName, UriKind.Absolute) 
            }; 
    } 
} 
  • (!*我不激動不已返回空呸的氣味)

無論如何,我會問以下問題給我自己:

  • 什麼可能打破這個代碼?是否有任何複雜的邏輯或脆弱的代碼要安全防範?
  • 有沒有什麼複雜的理解或值得通過測試代碼突出顯示代碼無法溝通?
  • 一旦我寫了這段代碼,我覺得我會再次訪問(更改)它的頻率如何?

IsXML函數非常微不足道。可能甚至不屬於這個班級。

如果LoadFile函數獲取有效的XML文件名,則會創建一個同步XmlDataProvide。

我首先會搜索誰使用LoadFile以及從哪裏傳遞fileName。如果它在我們的程序外部,那麼我們需要一些驗證。如果其內部和其他地方我們已經在進行驗證,那麼我們很好。正如馬丁所建議的,我建議重構這個以Uri作爲參數而不是字符串。

一旦我們解決了這個問題,那麼我們需要知道的是,是否存在XMLDataProvider處於同步模式的任何特殊原因。

現在,有什麼值得測試的嗎? XMLDataProvider不是我們構建的類,我們期望它在我們提供有效的Uri時正常工作。

所以坦白說,我會不浪費我的時間寫這個測試。在未來,如果我們看到更多的邏輯蔓延,我們可能會再次訪問。

+0

一般的優秀答案,但我不確定它適用於TDD的新手,你和我都是在這方面,但10年前想象自己。你會怎麼做? – 2012-05-31 18:35:54

+1

我不確定10年前我會做什麼。但這是我現在要做的,我會推薦其他人做。 – 2013-10-17 12:01:27

6

這裏的問題是你沒有做TDD。你先寫好產品代碼,現在你想測試它。

清除所有代碼並重新開始。先編寫一個測試,然後編寫通過該測試的代碼。然後寫下一個測試等。

你的目標是什麼?給定一個以「xml」結尾的字符串(爲什麼不是「.xml」?),您需要基於名稱爲該字符串的文件的XML數據提供程序。這是你的目標嗎?

第一次測試將是退化情況。給定一個像「name_with_wrong_ending」這樣的字符串,你的函數就會失敗。它應該如何失敗?它應該返回null嗎?還是應該拋出異常?你可以考慮這個問題並在你的測試中作出決定。然後你通過測試。

現在,怎麼樣這樣的字符串:「test_file.xml」,但在沒有這樣的文件存在的情況下?在這種情況下,你想要什麼功能?它應該返回null嗎?它應該拋出異常嗎?

測試這個最簡單的方法當然是在一個沒有該文件的目錄中運行代碼。但是,如果您寧願編寫測試以便它不使用文件系統(一個明智的選擇),那麼您需要能夠問「這個文件是否存在」這個問題,然後您的測試需要強制回答成爲「假」。

您可以通過在名爲「isFilePresent」或「doesFileExist」的類中創建一個新方法來實現。您的測試可以覆蓋該函數以返回「false」。現在,您可以在文件不存在的情況下測試您的'LoadFile'功能是否正常工作。

當然,現在您必須測試「isFilePresent」的正常實現是否正常工作。爲此,您必須使用真正的文件系統。但是,您可以通過創建一個名爲FileSystem的新類並將您的'isFilePresent'方法移動到新類中,從而保持文件系統測試不在LocalizationData測試中。然後,您的LocalizationData測試可以創建該新FileSystem類的衍生物並覆蓋「isFilePresent」以返回false。

您仍然需要測試FileSystem的常規實現,但這是一組不同的測試,只能運行一次。

好的,下一個測試是什麼?當文件確實存在但是不包含有效的xml時,你的'loadFile'函數是做什麼的?它應該做什麼?或者這是客戶的問題?你決定。但是如果你決定檢查它,你可以使用和以前一樣的策略。創建一個名爲isValidXML的函數,並讓測試覆蓋它以返回false。

最後我們需要編寫實際返回XMLDataProvider的測試。因此,在所有其他函數爲createXmlDataProvider之後,應該調用'loadData'的最終函數。你可以覆蓋它返回一個空的或虛擬的XmlDataProvider。

請注意,在您的測試中,您從未使用過真正的文件系統,並且真正基於文件創建了一個XMLDataProvider。但你所做的是檢查你的loadData函數中的每個if語句。您已經測試了loadData函數。

現在你應該再寫一個測試。使用真實文件系統和真正有效的XML文件的測試。

0

這一次,不要試圖打破你對文件系統的依賴。這種行爲顯然取決於文件系統,並且似乎處於與文件系統的集成點,因此請使用文件系統進行測試。

現在,我第二個鮑勃的建議:扔掉這些代碼,並嘗試試駕。這是一個偉大的練習,也是我訓練自己去做的。祝你好運。

0
  • ,而不是返回XmlDataProvider它關係到你的特定技術的,這個隱藏實現細節。它看起來像你需要一個倉庫角色對

    LocalizationData GetLocalizationData(PARAMS)

你就能得到這個角色,它在內部使用XML的實現。您需要編寫集成測試來測試XmlLocalizationDataRepository是否可以讀取實際的Xml數據存儲。 (慢)。

  • 你的代碼可以模擬出GetLocalizationData的其餘部分()