单元测试应该声明为内联吗?

3

我长期以来一直是Python的doctest库的粉丝,原因很简单,注释不仅有用而且可以在验证正确行为时使用。最近,我偶然发现了(似乎)鲜为人知的System.Diagnostics.ConditionalAttribute,这可以轻松地允许你在类本身内部定义类方法的测试。以下是一个简单的示例:

using System.Diagnostics;
using NUnit.Framework;

namespace ClassLibrary1
{
    public class Class1
    {
        public static int AddTwoNumbers(int x, int y)
        {
            return x + y;
        }

        [Conditional("DEBUG")]
        [TestCase(1, 1, 2)]
        [TestCase(1, 2, 3)]
        [TestCase(2, 1, 3)]
        [TestCase(11, 7, 18)]
        public static void TestAddTwoNumbers(int x, int y, int sum)
        {
            int actual = AddTwoNumbers(x, y);
            Assert.AreEqual(sum, actual);
        }
    }
}

通过这样做,您可以创建一个调试程序集来运行测试以及一个生产程序集,其中所有内容都被剥离,类似于FAKE可以构建项目的方式。问题是,您应该这样做吗?这是一个好习惯吗?为什么或为什么不是?

您还会发现,此示例实际上并未按照我所期望的那样工作。我不确定为什么属性允许编译测试方法。有任何想法吗?

4个回答

4
ConditionalAttribute 不会改变方法本身是否被编译,它只是改变是否生成对该方法的调用。
例如,Debug.WriteLine 被应用了 Conditional("DEBUG"),但代码仍然存在。重点在于,当没有定义 DEBUG 预处理器符号时,包含对 Debug.WriteLine 的调用的客户端代码将忽略这些调用。
要有条件地编译整个方法,您可以使用:
#if DEBUG
...
#endif

即使不考虑这个问题,我也不会这样做。我喜欢将生产代码与测试代码分开。我发现这样更清晰--尽管这意味着我不能测试私有方法。(有些人认为你根本不应该测试实现细节,但这是完全不同的问题。)
还有一个问题是针对真正的代码进行测试。如果你要构建带有内置测试和不带有内置测试的代码版本,并且在生产中使用不带测试的汇编,这意味着你正在运行未经测试的代码。当然,它可能会像定义DEBUG一样正常工作,但如果没有呢?我喜欢能够针对我在生产中使用的完全相同的二进制文件运行单元测试。

@Jon;以下建议是否会改变您的评估:(1)将测试用例与“#if XXX”结构内联(2)在构建期间编译包括和排除测试(=prod)(3)仅使用调试构建来运行包含测试用例的第二个程序集,该程序集不包括它们。好处:(1)您可以将测试用例与被测试的代码紧密结合(2)您可以部署一个经过测试的程序集,而不会混杂着测试。 - jerryjvl
@jerryjvl:不会,因为这属于我的最后一段 - 你不会部署一个经过测试的程序集,因为没有测试的构建不是你要运行测试的构建。 - Jon Skeet
@Jon: 我觉得混乱可能掩盖了我的观点。我会有 "AssemblyWithTests.dll" 和 "AssemblyWithoutTests.dll"(从几乎相同但不完全相同的源代码构建)...... 然后我会使用测试运行器来运行 "AssemblyWithTests.dll"中包含的测试,以针对 "AssemblyWithoutTests.dll" 进行部署。这样你既可以拥有蛋糕,也可以吃掉它... 我错过了什么吗? - jerryjvl
@jerryjvl:在编译时,当所有引用都在AssemblyWithTests内部时,您如何说服AssemblyWithTests使用AssemblyWithoutTests中的类? - Jon Skeet
啊,这正是我在寻找的... 这种方法存在的问题。我猜在那个时候,测试运行器会采用类似于DynamicProxy的方法。可惜了。 - jerryjvl
显示剩余2条评论

2

(IMO) 绝对不行。

虽然使用 Debug 属性可以使方法在发布项目中不被暴露,但事实是,在开发过程中(很可能是在 DEBUG 模式下),这将会使类变得非常混乱,具体取决于您开发的测试用例数量(即使是一个占用空间较小的类,您也可以开发许多测试用例)。

此外,我认为这是封装性差。您想为类编写测试,但这些测试实际上并没有帮助服务或增强类的实际功能,因此不应该成为它的一部分。

最后,单独的测试套件的一个伟大优势是,您可以在多个类之间建立复杂的相互依赖关系,从而测试这些类之间的交互。

在您的情况下,视野范围仅限于一个单独的类。如果您需要引入其他类(模拟实现等),那么您会把它们放在哪里?


根据客户需求,保持测试代码与生产代码完全分离可能非常重要。 - sourcenouveau
如果我同时模拟或测试多个类,我肯定不会将测试放在类内部。我认为这种方法只适用于小型函数。当然,这是一个很棘手的问题。 - user29439

2
这种方法有利有弊。
一方面,您将能够测试内部功能。有些人会认为这是一件好事。其他人会认为您应该只测试公共接口。个人认为,偶尔需要测试内部功能(如果没有其他原因,也可以隔离特定行为),但这是您可能可以通过InternalsVisibleTo实现的。
另一方面,您将能够测试内部功能。您的测试代码会表现得像它属于程序集一样。我个人认为它不是应用程序的一部分,也不应该在那里。把它看作“关注点分离”的一种形式。测试只测试,应用程序执行测试所测试的内容。但测试应该尽可能少地了解实现细节。如果测试在内部,它们很容易知道所有繁琐的细节。如果实现细节稍后更改,则测试将无效并且必须重新编写。
个人而言,我更喜欢使用外部程序集。这不仅可以强制确保我的测试仅测试软件,还使我问自己如何编写软件,以便它可以被外部源测试。这导致整体上更好的软件设计。我迄今为止从未后悔过。

非常好的观点。感谢您考虑了双方立场。我同意分离是一个好主意。 - user29439

0

我同意大多数其他答案:最好将单元测试保持独立。特别是要注意Jon关于ConditionalAttribute的观点:尤其是代码仍然存在,只是没有被调用。

然而,我认为你会喜欢Code Contracts。他们实际上使用了一个重写器,在编译后修改您的代码。这使您可以轻松设置包括各种运行时检查的Debug构建以及不包含任何该代码的Release构建。您还可以安装VS扩展程序,在代码编辑器中显示前置条件和后置条件。

我在我的博客上有一个简短的入门文章,其中我介绍了如何为库设置CC。我的风格是有一个Debug构建(带有完整的检查)和一个Release构建(没有检查,但包括一个包含前置条件的单独dll)。

请注意,Code Contracts不是单元测试的替代品,但它是一个很好的补充。


我知道并喜欢代码契约。这只是一个让测试更接近源代码的想法。 - user29439

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