我目前正在为包含验证例程的业务逻辑类编写一些单元测试。例如:
public User CreateUser(string username, string password, UserDetails details)
{
ValidateUserDetails(details);
ValidateUsername(username);
ValidatePassword(password);
// create and return user
}
我的测试夹具是否应该包含对Validate *方法中可能出现的每个可能的验证错误的测试,或者最好将其留给一组单独的测试?或许验证逻辑应该以某种方式重构?
我的理由是,如果我决定测试CreateUser中可能发生的所有验证错误,那么测试夹具将变得非常臃肿。大多数验证方法都在不止一个地方使用......
在这种情况下有任何好的模式或建议吗?
答案 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)
如果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)已经过自行测试,因此不需要过多的单元测试< /强>