静态测试对于单元测试而言是否普遍“邪恶”?如果是的话,为什么瑞沙珀推荐它?[关闭]


85

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

鉴于其中有两个不是免费的,而另一个没有发布1.0版,因此模拟静态内容并不是一件容易的事。

这是否构成静态方法和此类“邪恶”(就单元测试而言)?如果是这样,为什么harsharper希望我做任何可以静态的事情?(假设重新剃刀也不是“邪恶的”。)

澄清: 我是在谈论要对方法进行单元测试并且该方法在其他单元/类中调用静态方法的情况。根据单元测试的大多数定义,如果仅让被测方法在另一个单元/类中调用静态方法,则您不是单元测试,而是集成测试。(有用,但不是单元测试。)


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

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

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

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

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

Answers:


105

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

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


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

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

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

20
@Vaccano:具有输出或随机异常的静态方法具有副作用。
迪洪·杰维斯

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

26

您似乎在混淆静态数据和静态方法。如果我没记错的话,Resharper建议将private类中的方法设置为静态(如果可以这样做的话)-我相信这会带来很小的性能优势。它建议做“什么,可以是”静态的!

静态方法没有什么问题,并且易于测试(只要它们不更改任何静态数据)。例如,考虑一下数学库,它是使用静态方法的静态类的理想选择。如果您有这样的(人为)方法:

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

那么这是可以测试的,并且没有副作用。您只需要检查一下,当您传递20时就可以返回400。没问题。


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

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

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

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

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

18

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

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

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

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

9
幸运的是,我们也对代码的可读性和亲吻有疑问:)
gbjbaanb

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

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

15

静态不一定是邪恶的,但是当涉及到使用假冒/模仿/存根进行单元测试时,它们可能会限制您的选择。

有两种通用的模拟方法。

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

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

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


14

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

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


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

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

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

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

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

5

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

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

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

鉴于此,有三种选择:

  1. 将单元测试定义为对由类及其硬编码依赖项组成的单元的测试。这样做可以避免循环依赖。

  2. 永远不要在您负责测试的类之间创建编译时依赖性。这可以工作,只要您不介意所产生的代码样式。

  3. 不要单元测试,而是集成测试。只要它与使用集成测试一词所需要的其他功能不冲突,该方法即有效。


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

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

4

没有两种方法。如果您为所有代码编写隔离的原子单元测试,则不会经常使用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不像常规静态方法那样具有丰富的支持,因此该方法具有以下不良副作用:

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

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

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

那么,为什么C#具有静态方法?为什么它允许使用非虚拟实例方法?

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

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

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

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


3
  1. 我相信部分原因是静态方法比实例方法“调用”得更快。(用引号引起,因为这有微优化的味道)请参见http://dotnetperls.com/static-method
  2. 它告诉您它不需要状态,因此可以在任何地方调用它,如果这是某人唯一需要的东西,则可以消除声明开销。
  3. 如果要模拟它,那么我认为通常是在接口上声明它的惯例。
  4. 如果在接口上声明,则R#不会建议您将其设置为静态。
  5. 如果声明为虚拟,则R#也不建议您将其设置为静态。
  6. 静态保持状态(字段)是始终应谨慎考虑的事情。静态和螺纹像锂和水一样混合。

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

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


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

1
不,您不能在接口上声明静态。这是没有意义的。
MIA 2010年

3

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


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

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

1
与这个问题无关。
Igby Largeman 2014年

2

如果从其他方法内部调用了静态方法,则无法阻止或替代这样的调用。这意味着,这两种方法组成一个单元。任何形式的单元测试都会对它们进行测试。

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


0

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

现在,对于可测试性来说,这种情况不应该成为问题,因为您无论如何都不应该测试私有方法。

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


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

本质上访问实例变量意味着它不能是静态的。静态方法可能具有的唯一状态是类变量,并且应该发生的唯一原因是,如果您正在处理单例,则别无选择。
罗伦·佩希特尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.