2009-01-08 91 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 
} 

然后,您可以提供单独的单元测试纯粹的验证和你的业务逻辑类测试可以集中在验证通过,并在验证失败时上,以及作为你所有的其他测试。