单元测试和验证逻辑

时间:2009-01-08 14:54:53

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

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

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

    // create and return user
}

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

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

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

6 个答案:

答案 0 :(得分: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的dot net rocks剧集,在那里他谈到类似的方式。分组和命名测试http://www.dotnetrocks.com/default.aspx?showNum=406

我希望这更像你正在寻找的东西。

答案 1 :(得分:2)

  • 让针对Validate方法的单元测试(复数)确认其正确运行。
  • 让针对CreateUser方法的单元测试(复数)确认其正确运行。

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

答案 2 :(得分:2)

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

没有必要为所有可能的参数组合测试其他方法,只是为了确保执行验证。

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

验证通常是为了友好地通知用户他的输入不正确。它与业务逻辑密切相关(密码不够强,电子邮件格式不正确等)。

按合同设计可确保您的代码可以执行而不会在以后抛出异常(即使没有它们,您也会得到异常,但更晚,可能更加模糊)。

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

最后,当您想要友好地通知用户他有误时,请编写验证逻辑测试。否则,请使用“按合同设计”并继续抛出异常。

答案 3 :(得分: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
}

然后,您可以提供单独的单元测试纯粹进行验证,并且您对业务逻辑类的测试可以关注验证通过时和验证失败时,以及所有其他测试。

答案 4 :(得分:0)

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

答案 5 :(得分:0)

我正在使用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)已经过自行测试,因此不需要过多的单元测试< /强>