.NET 4.0代码合同 - 它们将如何影响单元测试?

38

例如,这个文章介绍了它们。

有什么好处呢?

静态分析听起来很酷,但同时它会防止在单元测试中将null作为参数传递。(如果您按照文章中的示例进行)

顺便说一下单元测试的话题——既然已经实践了自动化测试,那么使用代码约束是否毫无意义呢?

更新

尝试使用代码约束后我有些失望。例如,根据被接受的答案中的代码:

public double CalculateTotal(Order order)
{
    Contract.Requires(order != null);
    Contract.Ensures(Contract.Result<double>() >= 0);
    return 2.0;
}

针对单元测试,您仍需编写测试用例以确保不能传递 null 值,并且在业务逻辑中的约束条件下结果大于或等于零。换句话说,如果我删除第一个约束条件,除非我特别针对此功能编写了测试用例,否则不会导致任何测试用例失败。然而,这是基于未使用 Visual Studio 更好(ultimate等)版本内置的静态分析。

本质上,它们都归结为书写传统 if 语句的替代方式。我的使用经验显示了 TDD 和 Code Contracts 的原因以及我如何实现。


2
为什么你还需要编写测试来确保不能传递null?是因为你觉得静态验证器不可信吗?值得注意的是,order为null对该方法的功能没有影响,所以我个人认为将该参数视为错误。如果您实际上正在使用订单执行某些计算,则我期望其他测试会因NullReferenceException而失败。 - Stephen J. Anderson
@StephenJ.Anderson 对于 order 的精彩代码审查评论点赞。 - THBBFT
4个回答

38

我认为单元测试和合约并不会对彼此产生太大干扰,如果有的话,合约应该帮助单元测试,因为它消除了为无效参数添加繁琐重复测试的需要。合约指定函数可以提供的最小服务范围,而单元测试则试图验证特定输入集的实际行为。考虑以下假设的例子:


public class Order
{
    public IEnumerable Items { get; }
}

public class OrderCalculator
{
    public double CalculateTotal(Order order)
    {
        Contract.Requires(order != null);
        Contract.Ensures(Contract.Result<double>() >= 0);

        return 2.0;
    }
}

显然,该代码满足了契约,但你仍需要进行单元测试以验证其实际行为是否符合预期。


7
如果你使用了静态验证工具,就不需要为合同规定编写单元测试。Visual Studio 2010 Team edition将为.NET代码合同包括这些工具。如果静态验证器没有发出任何警告或错误,则代码符合合同要求。 - Wim Coenen
3
单元测试和合约并不是互斥的。合约能做的事情,测试做不到;而测试又需要确保合约正确,并且调用代码遵循它们。仅仅因为你有一个 "Requires" 语句,并不意味着你不应该对该条件进行测试。 - wekempf
1
我得出结论,编写测试以确保您的合同“正确”是不值得的。仅仅创建合同就意味着您正在思考null是否是有效参数。测试前提条件会分散注意力,而这并不是真正重要的事情;函数实际上所做的事情才是最重要的。我认为可以通过添加在接口上放置合同的示例来改进答案。您真的会为所有实现构建单元测试以确保拒绝null吗?因此,对于这个答案加一。 - Andy
我认为示例中呈现的代码合同是可以的,但它们并不完整。就好像你编写单元测试来测试相同的内容一样。总和应大于或等于0。这意味着您的合同不完整。据我所知,合同应该是完整的,因为它们应该提供所有先决条件/后置条件,在这些条件下,代码执行将提供预期的结果。我认为您的代码并不是合同使用的好例子。我敦促您也检查一下NUnit理论,因为它们本质上与合同非常相似,只是它们被呈现为测试。 - Robert Koritnik
2
我完全同意@wekempf的观点。如果我误输入了合同条件怎么办?合同是否正确?不是的。我会知道吗?也许。但是“也许”是不够的。没有像单元测试一样的东西能够确切地告诉我代码是否按照我的期望工作。当然,我也可能会打错单元测试。但是很有可能这个测试最终会因为这个错误而失败。但是对于一个合同来说,我的代码可能永远不会出现问题,但是由于打字错误,我本来想要的条件可能永远无法满足。 - fourpastmidnight
说如果我手残打错了代码合同是一个不好的论点。你也可以手残地编写单元测试,从而无法获得足够的代码覆盖率。单元测试仍然可能允许出现错误的代码。代码合同的绝妙之处在于它们可以应用于接口,并且静态检查将检查所有实现,而不会重复测试。 - Matthew Whited

27

什么是好处?

假设你想确保一个方法从不返回null。现在使用单元测试,您必须编写大量的测试用例,其中调用该方法并验证输出是否为非null。问题是,您无法测试所有可能的输入。

通过代码合同,您只需声明该方法永远不会返回null。静态分析器将在无法证明时发出警告。如果没有警告,就知道您的断言是正确的对于所有可能的输入。

更少的工作,完美的正确性保证。有什么不喜欢的呢?


代码合约基本上是代码内部的单元测试... 我遇到的问题是如何为算法编写合约?假设您有一个校验和验证器方法ValidateChecksum(inputWithChecksum)。要提供完整的合约,您必须仅出于确保代码功能而重新创建算法。如果您的算法存在错误,则您的合约很可能也会存在错误。那怎么办呢? - Robert Koritnik
@RobertKoritnik:合同不必指定完整的行为,仅指定和证明代码的某些属性(如不返回null)已经很有用了。为确保计算产生的输出完全符合预期,单元测试仍然是最好的方法。 - Wim Coenen

4
合同允许您表达代码的实际目的,而不是让编译器或代码的下一个读者使用任意随机参数作为定义。这样可以显著提高静态分析和代码优化的效率。
例如,如果我声明一个整数参数(使用合同符号)在1到10的范围内,并且在我的函数中有一个本地数组声明了相同的大小,由该参数进行索引,那么编译器可以确定不存在下标错误的可能性,从而生成更好的代码。
您可以在合同中声明null值是有效的。
单元测试的目的是动态验证代码是否达到了其所述的目的。仅仅因为您为函数编写了合同,并不意味着代码确实如此,或者静态分析可以验证代码确实如此。单元测试不会消失。

+1 你说得对。合约对其他开发人员和静态/运行时分析非常有帮助,前提是合约是正确的——除非你进行了适当的单元测试,否则你无法真正知道合约是否正确。除了合约之外,单元测试还验证即使满足合约条件,代码仍然按照你的意图执行。 - fourpastmidnight

3
好的,它不会干扰单元测试。但是如果从TDD的角度考虑,我认为它可能会改变标准流程:
1. 创建方法(只有签名) 2. 创建单元测试 -> 实现测试 3. 运行测试: 让测试失败 4. 实现方法,为了使其工作而进行技巧性的操作 5. 运行测试: 看到它通过 6. 重构您(可能混乱的)方法体 7. (重新运行测试以确保您没有破坏任何内容)
这将是非常全面的单元测试过程。在这种情况下,我认为您可以在第1和第2点之间插入代码契约,例如:
1. 创建方法(只有签名) 2. 为方法的输入参数插入代码契约 3. 创建单元测试 -> 实现测试 4. ...
目前看到的优势是,您可以更容易地编写单元测试,因为您不必检查每个可能的路径,因为已经考虑了一些路由,已经由您定义的契约考虑到了。它只是给您额外的检查,但它不会取代单元测试,因为通常代码中始终存在更多逻辑,需要使用单元测试来测试更多的路径。
另一个可能性是将代码契约添加到重构部分中。基本上是额外的保证方式。但这样做是冗余的,因为人们不喜欢做冗余的事情。

你是否仍然有测试来验证合同中所确认的内容? - Finglas
我想我不会测试那些已经由合同保证的东西。这样会有些“重复”的工作。因此,在实际编写单元测试之前,我也将它们放在了TDD循环中。 - Juri
@Juri,如果您输错了合同条件,例如Contract.Requires(x < 2)...哎呀,应该是x < 1?没有测试,您怎么知道代码是否真正做到了您想要的效果?我不认为合同足够。合同不能替代测试。通过IDE支持,它们可以让其他用户知道可以期望什么前/后置条件(假设这些条件是正确的)。运行时/静态检查器可以执行分析(再次假设合同是正确的)。除此之外,您仍然需要测试。 - fourpastmidnight
1
如果你写了一个糟糕的合同,那么有什么能阻止你写一个糟糕的测试呢? - Matthew Whited

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接