我发现只有三种方法可以对C#.NET中的静态单元测试(模拟/存根)依赖性进行测试:

Moles
TypeMock
JustMock

鉴于其中有两个不是免费的,而其中一个没有发布1.0版,因此模拟静态内容并不是一件容易的事。
这样做会使静态方法和类似的“邪恶”(在单元测试中)感)?如果是这样,为什么harsharper要我做任何可以静态的事情? (假设resharper也不是“邪恶的”。)澄清:
我正在谈论要对方法进行单元测试并且该方法在其他单元/类中调用静态方法的情况。根据单元测试的大多数定义,如果仅让被测方法在另一个单元/类中调用静态方法,则您不是单元测试,而是集成测试。 (有用,但不是单元测试。)

评论

TheLQ:可以。我相信他在谈论无法测试静态方法,因为很多时候它都涉及静态变量。因此在测试之后和之间更改状态。

我个人认为您对“单位”的定义太过分了。 “单位”应为“可以进行独立测试的最小单位”。那可能是一种方法,可能不止于此。如果静态方法没有状态且经过良好测试,则可以进行第二次单元测试,这不是(IMO)问题。

“就我个人而言,你认为“单位”的定义太过分了。”不,仅是他遵循标准用法,而您正在制定自己的定义。

“为什么harsharper要我做任何可以是静态的,静态的?” Resharper不想让您做任何事情。只是使您意识到修改是可能的,并且可能是代码分析POV所希望的。 Resharper不能代替您自己的判断!

@ acidzombie24。常规方法也可以修改静态状态,因此它们与静态方法一样“糟糕”。它们还可以以较短的生命周期修改状态,这使它们更加危险。 (我不赞成使用静态方法,但是关于状态修改的观点也对常规方法造成了打击,甚至更多)

#1 楼

查看此处的其他答案,我认为在持有静态状态或产生副作用的静态方法与仅返回值的静态方法之间可能会有一些混淆。

无状态且无副作用的静态方法应该易于进行单元测试。实际上,我认为这种方法是函数式编程的“穷人”形式。您将方法传递给对象或值,则它返回一个对象或值。而已。我完全看不到这些方法将如何对单元测试产生负面影响。

评论


静态方法是可以单元测试的,但是调用静态方法的方法又如何呢?如果调用方在另一个类中,则它们具有一个依赖关系,需要将其解耦以进行单元测试。

– Vaccano
2010-09-21 15:51

@Vaccano:但是,如果您编写不持有状态且没有副作用的静态方法,则它们在功能上或多或少都等同于存根。

–罗伯特·哈维(Robert Harvey)
2010-09-21 18:41

简单的也许。但是更复杂的函数可能会引发异常并产生意外的输出(应该在针对静态方法的单元测试中,而不是在针对静态方法的调用者的单元测试中捕获),或者至少这就是我导致人们相信我所读的文献。

– Vaccano
2010-09-21 19:53



@Vaccano:具有输出或随机异常的静态方法具有副作用。

– Tikhon Jelvis
2011年12月11日,下午3:55

@TikhonJelvis Robert在谈论输出;和“随机”异常不应是副作用,它们本质上是一种输出形式。关键是,每当您测试调用静态方法的方法时,都将包装该方法及其潜在输出的所有排列,而不能孤立地测试您的方法。

–妮可
2012年8月16日23:04



#2 楼

您似乎在混淆静态数据和静态方法。如果我没记错的话,Resharper建议将private方法设置为类中的静态方法(如果可以的话)-我相信这会带来很小的性能优势。它不建议将“任何可能成为静态的东西”静态化!例如,考虑一下数学库,它是使用静态方法的静态类的理想选择。如果您有这样的(人为)方法:

public static long Square(int x)
{
    return x * x;
}


,那么这是可以测试的并且没有副作用。您只需检查一下,例如传入20即可返回400。没问题。

评论


当另一个类调用此静态方法时会发生什么?在这种情况下看起来很简单,但是除了上面列出的三个工具之一之外,它是一个无法孤立的依赖项。如果您不隔离它,那么您就不是“单元测试”,而是“集成测试”(因为您正在测试不同单元“集成”在一起的程度)。

– Vaccano
2010-09-21 15:54

没发生什么事。为什么会这样? .NET框架充满了静态方法。您是说不能在要进行单元测试的任何方法中使用它们吗?

– Dan Diplo
2010-09-21 16:16

好吧,如果您的代码处于.NET Framework的生产级别/质量上,那么可以肯定,继续。但是,单元测试的重点是对单元进行单独测试。如果您还在其他单元中调用方法(无论它们是静态的还是其他方法),那么现在正在测试单元及其依赖项。我并不是说这不是有用的测试,但按照大多数定义,它不是“单元测试”。 (因为您现在正在测试被测单元以及具有静态方法的单元)。

– Vaccano
2010-09-21 16:23



大概您(或其他人)已经测试了静态方法,并证明它可以工作(至少符合预期),然后再编写更多代码。如果您接下来要测试的部分出现问题,那应该是您应该首先看的地方,而不是已经测试过的东西。

– cHao
2010-09-21在19:03



@Vaccano那么,Microsoft如何测试.NET Framework,是吗? Framework中的许多类都引用了其他类(例如System.Math)中的静态方法,更不用说大量的静态工厂方法等了。此外,您将永远无法使用任何扩展方法等。事实是,简单的函数像这样对于现代语言来说是基础。您可以孤立地测试它们(因为它们通常是确定性的),然后在您的类中使用它们而不必担心。这不成问题!

– Dan Diplo
2010-09-21在19:06



#3 楼

如果真正的问题是“如何测试此代码?”:

public class MyClass
{
   public void MethodToTest()
   {
       //... do something
       MyStaticClass.StaticMethod();
       //...more
   }
}


然后,只需重构代码并像往常一样注入对静态类的调用,例如this:

public class MyClass
{
   private readonly IExecutor _externalExecutor;
   public MyClass(_IExecutor executor)
   {
       _exeternalExecutor = executor;
   }

   public void MethodToTest()
   {
       //... do something
       _exetrnalExecutor.DoWork();
       //...more
   }
}

public class MyStaticClassExecutor : IExecutor
{
    public void DoWork()
    {
        MyStaticClass.StaticMethod();
    }
}


评论


幸运的是,我们也有关于代码可读性和基于SO的KISS的问题:)

– gbjbaanb
2011-11-15 18:42

代表会不会更容易并且做同样的事情?

– jk。
2012年11月29日17:56

@jk可能是,但是很难使用IoC容器。

–晴天
2012年11月29日在21:11

#4 楼

静态不一定是邪恶的,但在使用假冒/嘲笑/存根进行单元测试时,它们会限制您的选择。

有两种通用的模拟方法。

第一个(传统-由RhinoMocks,Moq,NMock2实现;手动模拟和存根也在该阵营中)依赖于测试接缝和依赖项注入。假设您正在对一些静态代码进行单元测试,并且它具有依赖性。在以这种方式设计的代码中经常发生的事情是,静态函数创建自己的依赖关系,从而使依赖关系反转。您很快就会发现,无法将模拟接口注入以这种方式设计的测试代码中。

第二个接口(模拟任何东西-由TypeMock,JustMock和Moles实现)依赖于.NET的Profiling API 。它可以拦截您的任何CIL指令,并用伪造品替换您的代码块。这使TypeMock和本阵营中的其他产品可以模拟任何东西:静态,密封类,私有方法-那些设计得不可测试的东西。

两种思想流派之间一直在争论不休。有人说,遵循SOLID原则和可测试性设计(通常包括轻松进行静态操作)。另一个说,买TypeMock不用担心。

#5 楼

检查一下:“静态方法是可测试性的致命手段”。参数的简短摘要:

要进行单元测试,您需要花费一小段代码,重新连接其依赖项并进行隔离测试。静态方法很难做到这一点,不仅是在它们访问全局状态的情况下,即使是它们只是调用其他静态方法也是如此。

评论


我不会在我的静态方法中保留全局状态或引起副作用,因此该参数与我无关。如果静态方法局限于简单的过程代码,并且以“通用”方式(如数学函数)起作用,那么您链接的文章将提出一个没有根据的斜率参数。

–罗伯特·哈维(Robert Harvey)
2010-09-21 14:53



@Robert Harvey-如何对使用另一个类的静态方法的方法进行单元测试(即,它依赖于静态方法)。如果您只是称呼它,那么您不是“单元测试”,而是“集成测试”

– Vaccano
2010-09-21 15:48

不要只是阅读博客文章,而是阅读许多不同意的评论。博客只是一种意见,不是事实。

– Dan Diplo
2010-09-21在16:10

@Vaccano:没有副作用或外部状态的静态方法在功能上等效于一个值。知道这一点,与对创建整数的方法进行单元测试没有什么不同。这是函数式编程为我们提供的关键见解之一。

–史蒂文·埃弗斯(Steven Evers)
2012年11月29日17:59

除非嵌入式系统能够正常工作,否则应用程序的错误可能会引起国际性事件,而IMO,则是在“可测试性”驱动体系结构时,在首先采用“每一个角落测试”方法之前就已经扭曲了它。我也厌倦了XP的所有版本都主导着每个对话。 XP不是权威,而是行业。这是单元测试的原始,明智的定义:python.net/crew/tbryan/UnitTestTalk/slide2.html

–埃里克·雷彭(Erik Reppen)
13年5月21日在20:35

#6 楼

很少被承认的简单事实是,如果一个类包含对另一个类的编译器可见的依赖项,则不能与该类隔离地对其进行测试。您可以伪造看起来像测试的东西,并且会像测试一样出现在报告中。

但是它没有定义测试属性的关键;当事情出错时失败,而在事情正确时失败。

这适用于任何静态调用,构造函数调用,以及对不是从基类或接口继承的方法或字段的任何引用。如果类名出现在代码中,则它是编译器可见的依赖项,如果没有它,您将无法进行有效的测试。任何较小的块都根本不是有效的可测试单元。任何试图将其视为真实结果的尝试,都没有比编写一个小的实用程序来发出测试框架用来说“测试通过”的XML有意义的结果。

鉴于有三点选项:


将单元测试定义为对由类组成的单元及其硬编码依赖项的测试。这样做可以避免循环依赖。
永远不要在负责测试的类之间创建编译时依赖。如果您不介意所产生的代码样式,则此方法有效。
不要进行单元测试,而应该进行集成测试。只要它与使用术语“集成测试”所需的其他功能不冲突,该方法即有效。


评论


没有什么可以说单元测试是对单个类的测试。这是对单个单元的测试。单元测试的定义属性是它们快速,可重复且独立。通过任何合理的定义,在方法中引用Math.Pi都不会使其成为集成测试。

–萨拉
16年6月2日在12:01

即选项1。我同意这是最好的方法,但是了解其他人以不同的方式(合理地或不合理地)使用术语可能会很有用。

– soru
16年6月2日在14:57

#7 楼

没有两种方法。如果您为所有代码编写独立的原子单元测试,则不会经常使用ReSharper的建议和C#的一些有用功能。例如,如果您有一个静态方法,则需要对它进行存根除非您使用基于配置文件的隔离框架,否则您将无法使用。兼容呼叫的解决方法是更改​​方法的顶部以使用lambda表示法。例如:

之前:

    public static DBConnection ConnectToDB( string dbName, string connectionInfo ) {
    }


之后:

    public static Func<string, string, DBConnection> ConnectToDB (dbName, connectionInfo ) {
    };


两个是呼叫兼容的。呼叫者不必更改。函数的主体保持不变。

然后在单元测试代码中,可以像这样对这个调用进行存根(假定它在一个名为Database的类中):

        Database.ConnectToDB = (dbName, connectionInfo) => { return null|whatever; }


完成后请小心将其替换为原始值。您可以通过try / finally进行操作,也可以在每次单元测试后清理每次测试后调用的代码,编写如下代码:

    [TestCleanup]
    public void Cleanup()
    {
        typeof(Database).TypeInitializer.Invoke(null, null);
    }


这将重新调用您类的静态初始化器。

Lambda Funcs不像常规静态方法那样受支持,因此这种方法具有以下不良副作用:


如果静态方法是扩展方法,则必须首先将其更改为非扩展方法。 Resharper可以自动为您执行此操作。
如果静态方法的任何数据类型都是嵌入式互操作程序集(例如Office),则必须包装方法,包装类型或将其更改为类型' object'。
您将无法再使用Resharper的更改签名重构工具。

但是,您可以完全避免使用静态变量,并将其转换为实例方法。除非该方法是虚拟的或作为接口的一部分实现的,否则它仍然不可模仿。

因此,实际上,有人建议对静态方法进行存根的补救措施是使它们成为实例方法,它们也将针对非虚拟或接口一部分的实例方法。静态方法?
为什么允许使用非虚拟实例方法?

如果使用这两个“功能”之一,则根本无法创建隔离方法。

那么,什么时候使用它们?

将它们用于任何您不希望任何人都想使用的代码。一些示例:
String类的Format()方法
Console类的WriteLine()方法
Math类的Cosh()方法

还有一件事情..大多数人不会在意这一点,但是如果您可以在意间接调用的性能,那是避免使用实例方法的另一个原因。在某些情况下,它会影响性能。这就是为什么非虚拟方法首先存在的原因。

#8 楼


我相信这部分是因为静态方法比实例方法“更快速”地被调用。 (用引号引起,因为这有微优化的味道)请参阅http://dotnetperls.com/static-method

,它告诉您不需要状态,因此可以从任何地方调用,删除如果这是某人唯一需要的东西,那么它就会产生开销。
如果我要模拟它,那么我认为通常是在接口上声明它的惯例。
如果在接口上声明,则R#不会建议您将其设置为静态。
如果将其声明为虚拟,则R#也将不会建议您将其设置为静态。 (字段)静态是始终应仔细考虑的问题。静态和螺纹像锂和水一样混合。

R#不是唯一可以提出此建议的工具。 FxCop / MS代码分析也将执行相同的操作。

我通常会说,如果该方法是静态的,则通常也应该是可测试的。这带来了一些设计上的考虑,并且可能比我现在所进行的讨论更多,因此请耐心等待反对票和评论...;)

评论


您可以在接口上声明静态方法/对象吗? (我不这么认为)。以其他方式,我指的是被测试方法何时调用您的静态方法。如果调用方位于其他单元中,则需要隔离静态方法。使用上面列出的三个工具之一很难做到这一点。

– Vaccano
2010-09-21 15:56



不,您不能在接口上声明静态。这将是毫无意义的。

– MIA
2010-09-21 17:24

#9 楼

我看到很长一段时间以后,还没有人说出一个非常简单的事实。如果resharper告诉我可以将一个方法静态化,那么这对我来说意义重大,我可以听到他的声音告诉我:“嘿,您,这些逻辑部分不是当前类的责任,因此应避免使用在一些帮助类或其他东西中。”

评论


我不同意。在大多数时候,Resharper说我可以使某些东西静态化时,该类中的两个或多个方法通用一些代码,因此我将其提取到自己的过程中。将其转移到帮助者上将毫无意义。

–Loren Pechtel
2012年11月30日,下午3:21

仅当域非常简单并且不适合将来修改时,我才能看到您的观点。不同的是,您所说的“无意义的复杂性”对我来说是一个很好的且易于理解的设计。在某种程度上,拥有一个简单而明确的存在原因的帮助程序类是SoC和“单一职责”原则的“口头禅”。另外,考虑到这个新类成为主类的依赖,它必须公开一些公共成员,并且自然而然地成为一个可测试的对象,并且当充当依赖时很容易被嘲笑。

– g1ga
2012年11月30日10:30

与这个问题无关。

–伊比·拉格曼(Igby Largeman)
2014年12月12日22:28

#10 楼

如果从其他方法内部调用了静态方法,则无法阻止或替代这样的调用。这意味着,这两种方法组成一个单元。

如果此静态方法与Internet通讯,连接数据库,显示GUI弹出窗口或以其他方式将Unit Test转换为完全混乱,那么它将无法进行任何轻松的变通。调用这种静态方法的方法即使不进行重构也无法测试,即使它具有大量纯计算代码,这些代码也将从单元测试中受益匪浅。

#11 楼

我相信Resharper会为您提供指导,并应用已设置的编码指导。当我使用Resharper并告诉我一个方法应该是静态的时,它必然会在一个不作用于任何实例变量的私有方法上。

现在关于可测试性,这种情况不应该这是一个问题,因为您无论如何都不应该测试私有方法。

关于公共静态方法的可测试性,当静态方法达到静态时,单元测试就变得很困难。我个人将其保持在最低限度,并在将任何依赖项传递到可通过测试治具进行控制的方法中时,尽可能将静态方法用作纯函数。但是,这是设计决定。

评论


我指的是当您有一个被测方法(非静态)在另一个类中调用静态方法时。只能使用上面列出的三个工具之一来隔离该依赖性。如果不隔离它,那么您就是集成测试,而不是单元测试。

– Vaccano
2010-09-21 15:52

本质上访问实例变量意味着它不能是静态的。静态方法可能具有的唯一状态是类变量,并且应该发生的唯一原因是如果您正在处理单例并且因此没有选择的余地。

–Loren Pechtel
2012年11月30日,下午3:25