2009-01-08 166 views
10

我目前正在爲包含驗證例程的業務邏輯類編寫一些單元測試。例如:單元測試和驗證邏輯

public User CreateUser(string username, string password, UserDetails details) 
{ 
    ValidateUserDetails(details); 
    ValidateUsername(username); 
    ValidatePassword(password); 

    // create and return user 
} 

如果我的測試夾具包含可能出現在驗證方法*,或者是它更好地留給了一套獨立的測試每一種可能的驗證錯誤的測試?或者驗證邏輯應該以某種方式重構?

我的推理是,如果我決定測試CreateUser中可能出現的所有驗證錯誤,測試夾具將變得相當臃腫。並且大多數驗證方法都是從多個地方使用的...

這種情況下的任何出色模式或建議?

回答

11

每個測試應該只會因爲一個原因而失敗,並且只有一個測試會因爲這個原因而失敗。

這有助於編寫一套可維護的單元測試。

我會爲ValidateUserDetails,ValidateUsername和ValidateUserPassword分別編寫幾個測試。然後你只需要測試CreateUser調用這些函數。


重新閱讀你的問題;似乎我誤解了一些事情。

您可能對J.P Boodhoo在他的行爲驅動設計風格上所寫的內容感興趣。 http://blog.developwithpassion.com/2008/12/22/how-im-currently-writing-my-bdd-style-tests-part-2/

BDD正成爲一個超負荷的術語,每個人都有不同的定義和不同的工具來做到這一點。據我所知,JP Boodhoo正在做的是根據關注而不是課堂來分解測試夾具。

例如,您可以爲測試創建單獨的燈具驗證用戶詳細信息,驗證用戶名,驗證密碼和創建用戶。 BDD的想法是,通過命名testfixtures並測試正確的方法,您可以通過打印testfixture名稱和測試名稱來創建幾乎像文檔一樣的東西。按照關注點而不是按課程對測試進行分組的另一個好處是,對於每個燈具,您可能只需要一個設置和拆卸例程。

雖然我自己並沒有太多的經驗。

如果您有興趣閱讀更多內容,JP Boodhoo在他的博客上發佈了很多關於此內容的內容(請參閱上面的鏈接),或者您也可以與Scott Bellware一起聽點網絡劇集,他會談論類似的方式的分組和命名測試http://www.dotnetrocks.com/default.aspx?showNum=406

我希望這更符合您的需求。

+0

我重新說了一下我的問題 - 請再讀一遍 – JacobE 2009-01-08 15:06:46

2
  • 讓單元測試(複數)對驗證方法確認其正確的功能。
  • 讓單元測試(複數)對CreateUser方法確認其正確的功能。

如果僅需要CreateUser調用驗證方法,但不需要自己做出驗證決策,那麼針對CreateUser的測試應確認該需求。

2

您肯定需要測試驗證方法。

爲了確保執行驗證,不需要爲所有可能的參數組合測試其他方法。

您似乎在按合同混合驗證和設計。

驗證通常執行以友好通知用戶他的輸入是不正確的。它與業務邏輯密切相關(密碼不夠強,電子郵件格式不正確等)。

按合同設計確保你的代碼可以在不拋出異常的情況下執行(即使沒有它們,你也會得到異常,但會晚得多,也許更隱晦)。

關於應該包含驗證邏輯的應用層,可能最好的是service layer (by Fowler),它定義了應用程序的邊界,並且是清理應用程序輸入的好地方。在這個範圍內不應該有任何驗證邏輯,只有Design By Contract才能在早期檢測錯誤。

因此最後寫驗證邏輯測試,當你想友好地通知用戶他誤解了。否則使用Design By Contract並繼續拋出異常。

0

我會爲每個ValidateXXX方法添加一堆測試。然後在CreateUser中創建3個測試用例,用於檢查每個ValidateUserDetails,ValidateUsername和ValidatePassword失敗但是另一個成功時會發生什麼。

0

我使用Lokad Shared Library來定義業務驗證規則。以下是我測試的情況(從開源樣品):

[Test] 
public void Test() 
{ 
    ShouldPass("[email protected]", "pwd", "http://ws.lokad.com/TimeSerieS2.asmx"); 
    ShouldPass("[email protected]", "pwd", "http://127.0.0.1/TimeSerieS2.asmx"); 
    ShouldPass("[email protected]", "pwd", "http://sandbox-ws.lokad.com/TimeSerieS2.asmx"); 

    ShouldFail("invalid", "pwd", "http://ws.lokad.com/TimeSerieS.asmx"); 
    ShouldFail("[email protected]", "pwd", "http://identity-theift.com/TimeSerieS2.asmx"); 
} 

static void ShouldFail(string username, string pwd, string url) 
{ 
    try 
    { 
    ShouldPass(username, pwd, url); 
    Assert.Fail("Expected {0}", typeof (RuleException).Name); 
    } 
    catch (RuleException) 
    { 
    } 
} 

static void ShouldPass(string username, string pwd, string url) 
{ 
    var connection = new ServiceConnection(username, pwd, new Uri(url)); 
    Enforce.That(connection, ApiRules.ValidConnection); 
} 

凡ValidConnection規則定義爲:如果某些失敗的情況下被發現

public static void ValidConnection(ServiceConnection connection, IScope scope) 
{ 
    scope.Validate(connection.Username, "UserName", StringIs.Limited(6, 256), StringIs.ValidEmail); 
    scope.Validate(connection.Password, "Password", StringIs.Limited(1, 256)); 
    scope.Validate(connection.Endpoint, "Endpoint", Endpoint); 
} 

static void Endpoint(Uri obj, IScope scope) 
{ 
    var local = obj.LocalPath.ToLowerInvariant(); 
    if (local == "/timeseries.asmx") 
    { 
    scope.Error("Please, use TimeSeries2.asmx"); 
    } 
    else if (local != "/timeseries2.asmx") 
    { 
    scope.Error("Unsupported local address '{0}'", local); 
    } 

    if (!obj.IsLoopback) 
    { 
    var host = obj.Host.ToLowerInvariant(); 
    if ((host != "ws.lokad.com") && (host != "sandbox-ws.lokad.com")) 
     scope.Error("Unknown host '{0}'", host); 
    } 

(即:新的有效連接網址添加),然後規則和測試得到更新。

有關此模式的更多信息,請參閱this article。一切都是開源的,所以請隨時重複使用或提出問題。

PS:注意,在這種樣品複合規則中使用原始規則(即StringIs.ValidEmail或StringIs.Limited)充分自行測試並因而不需要過度的單元測試

2

您的業務邏輯類的職責是什麼?它除了驗證之外還有其他事情嗎?我想我會試圖將驗證例程移入其自己的(UserValidator)或多個類(UserDetailsValidator + UserCredentialsValidator)的類中,具體取決於您的上下文,然後爲測試提供模擬。所以,現在你的類看起來是這樣的:

public User CreateUser(string username, string password, UserDetails details) 
{ 
    if (Validator.isValid(details, username, password)) { 
     // what happens when not valid 
    } 

    // create and return user 
} 

然後,您可以提供單獨的單元測試純粹的驗證和你的業務邏輯類測試可以集中在驗證通過,並在驗證失敗時上,以及作爲你所有的其他測試。