【问题标题】:Unit-tests and validation logic单元测试和验证逻辑
【发布时间】:2010-09-30 06:50:45
【问题描述】:

我目前正在为包含验证例程的业务逻辑类编写一些单元测试。例如:

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

    // create and return user
}

我的测试夹具是否应该包含针对 Validate* 方法中可能发生的每个可能的验证错误的测试,还是将其留给单独的一组测试更好?或者也许应该以某种方式重构验证逻辑?

我的理由是,如果我决定测试 CreateUser 中可能发生的所有验证错误,那么测试夹具将变得非常臃肿。而且大多数验证方法都在不止一个地方使用...

在这种情况下有什么好的模式或建议吗?

【问题讨论】:

    标签: c# .net unit-testing validation tdd


    【解决方案1】:

    每个测试都应该只因为一个原因而失败,并且只有一个测试应该因为那个原因而失败。

    这对编写一组可维护的单元测试很有帮助。

    我会为 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 的想法是,通过以正确的方式命名测试装置和测试,您可以通过打印出测试装置名称和测试名称来创建几乎读起来像文档的东西。按关注点而不是按类对测试进行分组的另一个优点是,您可能只需要为每个夹具设置一个设置和拆卸例程。

    不过,我自己对此并没有太多经验。

    如果您有兴趣阅读更多内容,JP Boodhoo 在他的博客上发布了很多关于此的内容(请参阅上面的链接),或者您也可以收听 Scott Bellware 的 dot net Rocks 插曲,其中他谈到了类似的方式分组命名测试http://www.dotnetrocks.com/default.aspx?showNum=406

    我希望这是您正在寻找的更多内容。

    【讨论】:

      【解决方案2】:

      你肯定需要测试验证方法。

      无需为所有可能的参数组合测试其他方法,以确保执行验证。

      您似乎在混合验证和合同设计。

      验证通常用于友好地通知用户他的输入不正确。跟业务逻辑有很大关系(密码不够强,邮件格式不对等)。

      契约式设计确保您的代码可以在以后执行而不会引发异常(即使没有它们,您也会得到异常,但要晚得多,而且可能更模糊)。

      关于应包含验证逻辑的应用程序层,可能最好的是service layer (by Fowler),它定义了应用程序边界并且是清理应用程序输入的好地方。并且在这个边界内不应该有任何验证逻辑,只有按合同设计才能更早地检测到错误。

      所以最后,当你想友好地通知用户他错了时,编写验证逻辑测试。否则使用按合同设计并继续抛出异常。

      【讨论】:

        【解决方案3】:
        • 让针对 Validate 方法的单元测试(复数)确认其功能正确。
        • 让针对 CreateUser 方法的单元测试(复数)确认其功能正确。

        如果只需要 CreateUser 调用验证方法,但不需要自己做出验证决定,那么针对 CreateUser 的测试应该确认该要求。

        【讨论】:

          【解决方案4】:

          您的业务逻辑类的职责是什么?除了验证之外,它还做些什么?我想我很想根据您的上下文将验证例程移动到它自己的一个类(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
          }
          

          然后,您可以纯粹为验证提供单独的单元测试,并且您的业务逻辑类测试可以专注于验证何时通过和验证失败,以及所有其他测试。 p>

          【讨论】:

            【解决方案5】:

            我会为每个 ValidateXXX 方法添加一堆测试。然后在 CreateUser 中创建 3 个测试用例来检查当 ValidateUserDetails、ValidateUsername 和 ValidatePassword 中的每一个失败但另一个成功时会发生什么。

            【讨论】:

              【解决方案6】:

              我使用Lokad Shared Library 来定义业务验证规则。以下是我测试极端案例的方法(来自开源的示例):

              [Test]
              public void Test()
              {
                ShouldPass("rinat.abdullin@lokad.com", "pwd", "http://ws.lokad.com/TimeSerieS2.asmx");
                ShouldPass("some@nowhere.net", "pwd", "http://127.0.0.1/TimeSerieS2.asmx");
                ShouldPass("rinat.abdullin@lokad.com", "pwd", "http://sandbox-ws.lokad.com/TimeSerieS2.asmx");
              
                ShouldFail("invalid", "pwd", "http://ws.lokad.com/TimeSerieS.asmx");
                ShouldFail("rinat.abdullin@lokad.com", "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);
                }
              

              如果发现一些失败案例(即:添加了新的有效连接 url),则规则和测试将被更新。

              可以在this article 中找到有关此模式的更多信息。一切都是开源的,所以请随意重用或提出问题。

              PS:请注意,此示例复合规则(即 StringIs.ValidEmail 或 StringIs.Limited)中使用的原始规则都经过了全面测试,因此不需要过多的单元测试。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-01-13
                • 1970-01-01
                • 2012-02-05
                • 1970-01-01
                • 2011-11-19
                相关资源
                最近更新 更多