简体   繁体   English

单元测试和验证逻辑

[英]Unit-tests and validation logic

I am currently writing some unit tests for a business-logic class that includes validation routines. 我目前正在为包含验证例程的业务逻辑类编写一些单元测试。 For example: 例如:

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

    // create and return user
}

Should my test fixture contain tests for every possible validation error that can occur in the Validate* methods, or is it better to leave that for a separate set of tests? 我的测试夹具是否应该包含对Validate *方法中可能出现的每个可能的验证错误的测试,或者最好将其留给一组单独的测试? Or perhaps the validation logic should be refactored out somehow? 或许验证逻辑应该以某种方式重构?

My reasoning is that if I decide to test for all the validation errors that can occur within CreateUser, the test fixture will become quite bloated. 我的理由是,如果我决定测试CreateUser中可能发生的所有验证错误,那么测试夹具将变得非常臃肿。 And most of the validation methods are used from more than one place... 大多数验证方法都是在不止一个地方使用的......

Any great patterns or suggestions in this case? 在这种情况下有任何好的模式或建议吗?

Every test should only fail for one reason and only one test should fail for that reason. 每个测试应该只因一个原因而失败,因此只有一个测试失败。

This helps a lot with writing a maintainable set of unit tests. 这有助于编写一组可维护的单元测试。

I'd write a couple of tests each for ValidateUserDetails, ValidateUsername and ValidateUserPassword. 我将为ValidateUserDetails,ValidateUsername和ValidateUserPassword分别编写几个测试。 Then you only need to test that CreateUser calls those functions. 然后,您只需要测试CreateUser是否调用这些函数。


Re read your question; 重读你的问题; Seems I misunderstood things a bit. 似乎我误解了一些事情。

You might be interested in what JP Boodhoo has written on his style of behaviour driven design. 您可能对JP Boodhoo在他的行为驱动设计风格上所写的内容感兴趣。 http://blog.developwithpassion.com/2008/12/22/how-im-currently-writing-my-bdd-style-tests-part-2/ http://blog.developwithpassion.com/2008/12/22/how-im-currently-writing-my-bdd-style-tests-part-2/

BDD is becoming a very overloaded term, everyone has a different definition and different tools to do it. BDD正在成为一个非常重载的术语,每个人都有不同的定义和不同的工具来做到这一点。 As far as I see what JP Boodhoo is doing is splitting up test fixtures according to concern and not class. 据我所知,JP Boodhoo正在做的是根据关注而不是上课来分割测试装置。

For example you could create separate fixtures for testing Validation of user details, Validation of username, Validation of password and creating users. 例如,您可以创建单独的灯具来测试用户详细信息的验证,用户名验证,密码验证和创建用户。 The idea of BDD is that by naming the testfixtures and tests the right way you can create something that almost reads like documentation by printing out the testfixture names and test names. BDD的想法是,通过命名testfixtures并以正确的方式测试,您可以通过打印出testfixture名称和测试名称来创建几乎像文档一样的东西。 Another advantage of grouping your tests by concern and not by class is that you'll probably only need one setup and teardown routine for each fixture. 通过关注而不是按类分组测试的另一个优点是,您可能只需要为每个夹具设置一个设置和拆卸例程。

I havn't had much experience with this myself though. 我自己也没有多少经验。

If you're interested in reading more, JP Boodhoo has posted a lot about this on his blog (see above link) or you can also listen to the dot net rocks episode with Scott Bellware where he talks about a similar way of grouping and naming tests http://www.dotnetrocks.com/default.aspx?showNum=406 如果你有兴趣阅读更多内容,JP Boodhoo已经在他的博客上发布了很多相关内容(见上面的链接),或者你也可以听听Scott Bellware的dot net rocks剧集,在那里他谈到类似的分组和命名方式测试http://www.dotnetrocks.com/default.aspx?showNum=406

I hope this is more what you're looking for. 我希望这更像是你在寻找的东西。

  • Let Unit Tests (plural) against the Validate methods confirm their correct functioning. 让单元测试(复数)反对验证方法确认其正确运行。
  • Let Unit Tests (plural) against the CreateUser method confirm its correct functioning. 让针对CreateUser方法的单元测试(复数)确认其正确运行。

If CreateUser is merely required to call the validate methods, but is not required to make validation decisions itself, then the tests against CreateUser should confirm that requirement. 如果CreateUser仅需要调用validate方法,但不需要自己做出验证决定,那么针对CreateUser的测试应确认该要求。

You definitely need to test validation methods. 你肯定需要测试验证方法。

There is no need to test other methods for all possible combinations of arguments just to make sure validation is performed. 没有必要为所有可能的参数组合测试其他方法,只是为了确保执行验证。

You seem to be mixing Validation and Design by Contract. 您似乎正在按合同混合验证和设计。

Validation is usually performed to friendly notify user that his input is incorrect. 通常执行验证以友好地通知用户他的输入不正确。 It is very related to business logic (password is not strong enough, email has incorrect format, etc.). 它与业务逻辑密切相关(密码不够强,电子邮件格式不正确等)。

Design by Contract makes sure your code can execute without throwing exceptions later on (even without them you would get the exception, but much later and probably more obscure one). 按合同设计可以确保您的代码可以执行而不会在以后抛出异常(即使没有它们,您也会得到异常,但是很晚以后可能会更加模糊)。

Regarding application layer that should contain validation logic, probably the best is service layer (by Fowler) which defines application boundaries and is a good place to sanitize application input. 关于应该包含验证逻辑的应用层,可能最好的是服务层(由Fowler)定义应用程序边界,并且是清理应用程序输入的好地方。 And there should not be any validation logic inside this boundaries, only Design By Contract to detect errors earlier. 并且在这个边界内不应该有任何验证逻辑,只有Design By Contract才能更早地检测错误。

So finally, write validation logic tests when you want to friendly notify user that he has mistaken. 所以最后,当你想友好地通知用户他有误时,写下验证逻辑测试。 Otherwise use Design By Contract and keep throwing exceptions. 否则使用Design By Contract并继续抛出异常。

What is the responsibility of your business logic class and does it do something apart from the validation? 您的业​​务逻辑类有什么责任,除了验证之外它还能做些什么? I think I'd be tempted to move the validation routines into a class of its own (UserValidator) or multiple classes (UserDetailsValidator + UserCredentialsValidator) depending on your context and then provide mocks for the tests. 我想我很想将验证例程移动到它自己的类(UserValidator)或多个类(UserDetailsValidator + UserCredentialsValidator)中,具体取决于您的上下文,然后为测试提供模拟。 So your class now would look something like: 所以你的班级现在看起来像:

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

    // create and return user
}

You can then provide seperate unit tests purely for the validation and your tests for the business logic class can focus on when validation passes and when validation fails, as well as all your other tests. 然后,您可以提供纯粹用于验证的单独单元测试,并且您对业务逻辑类的测试可以关注验证通过和验证失败时的重点,以及所有其他测试。

I would add a bunch of test for each ValidateXXX method. 我会为每个ValidateXXX方法添加一堆测试。 Then in CreateUser create 3 test cases for checking what happens when each of ValidateUserDetails, ValidateUsername and ValidatePassword fails but the other succeed. 然后在CreateUser中创建3个测试用例,以检查当ValidateUserDetails,ValidateUsername和ValidatePassword中的每一个失败但另一个成功时会发生什么。

I'm using Lokad Shared Library for defining business validation rules. 我正在使用Lokad共享库来定义业务验证规则。 Here's how I test corner cases (sample from the open-source): 以下是我测试角落案例的方法(来自开源的样本):

[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);
}

Where ValidConnection rule is defined as: 其中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);
  }

If some failing case is discovered (ie: new valid connection url is added), then the rule and the test gets updated. 如果发现了一些失败的情况(即:添加了新的有效连接URL),则规则和测试会更新。

More on this pattern could be found in this article . 有关此模式的更多信息,请参阅本文 Everything is Open Source so feel free to reuse or ask questions. 一切都是开源的,所以可以随意重用或提问。

PS: note that primitive rules used in this sample composite rule (ie StringIs.ValidEmail or StringIs.Limited) are thoroughly tested on their own and thus do not need excessive unit tests . PS:请注意,此示例复合规则中使用的原始规则 (即StringIs.ValidEmail或StringIs.Limited)会自行进行全面测试,因此不需要过多的单元测试

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM