静态方法:何时使用,何时不使用

10
我对TDD和DDD都很陌生,关于静态方法,我有一个简单的问题。大多数TDD大师一句话就说静态方法不好(我们应该忘记以前创建的大量静态工具,因为它们不可测试)。我可以理解为什么它们不可测试(这里可以找到一篇很好的澄清文章here,供感兴趣的人阅读,但我想我是唯一的新手:(),但我想知道从TDD的角度来看是否有一个好的、干净的指南来使用静态方法?

这可能对你们大多数人来说是非常愚蠢的问题,但一些提示会很棒,我只是想知道这里的专家们如何看待静态内容。提前感谢您。

编辑:在寻找答案时,我发现了另外两个关于静态使用的好帖子(虽然不涉及TDD),我想对那些感兴趣的人阅读一下(包括我自己)。

3个回答

11

我认为您可能有些误解。

静态方法是可测试的。以这个方法为例:

public static int Add(int x, int y)
{
    return x + y;
}
你可以通过检查返回值是否符合传入的参数来测试它。
当需要引入模拟(mock)时,静态方法会变得麻烦。例如,假设我有一些调用 File.Delete() 静态方法的代码。为了在测试中不依赖于文件系统,我想要替换/模拟这个调用,并使用一个测试版本来验证它是否从被测试的代码中调用过。如果我有一个实例对象,在该对象上调用 Delete() 就容易做到这点。但是大多数(或者所有?)mock框架不能mock静态方法,所以在我的代码中使用静态方法会强制我以不同的方式进行测试(通常是调用真正的静态方法)。
为了测试这样的东西,我将介绍一个接口:
interface IFileDeleter
{
    void Delete(string file);
}

然后,我的代码将采用实现此接口的对象实例(可以在方法调用中或作为构造函数中的参数),并调用其Delete()方法来执行删除操作:

void MyMethod(string file)
{
    // do whatever...
    deleter.Delete(file);
}

为了测试这个,我可以模拟 IFileDeleter 接口,并简单验证它的 Delete() 方法是否被调用。这样就不需要将真实的文件系统作为测试的一部分。

这看起来可能会使代码更加复杂(确实如此),但是通过这种方式进行测试将使得测试变得非常容易。


+1!Typemock可以模拟静态方法,但最好避免使用它们(当需要模拟时)。 - TrueWill
+1 非常感谢您提供的全面回答。为了分享知识,我会等待更多的答案(如果有的话)再选择一个答案。 - MSI
嗯,现在我读了你的回答,我问的确有点愚蠢。是的,你说得对,当我提出问题时,我有点错过了重点。当真正的麻烦出现时,它是嘲笑的;特别是当它只是一个实用方法时,不是测试方法本身。 - MSI

8
避免使用静态变量确实是正确的选择,但当你不能或者正在处理遗留代码时,以下选项可供选择。继承自adrianbanks上面的答案,假设你有以下代码(抱歉,它是用Java编写的,因为我不知道C#):
public void someMethod() {
   //do somethings
   File.delete();
   //do some more things
}

您可以将File.delete()重构为自己的方法,像这样:

public void someMethod() {
   //do somethings
   deleteFile();
   //do some more things
}

//protected allows you to override in a subclass
protected void deleteFile() { 
   File.delete();
}

然后,在准备单元测试时,创建一个扩展原始类并存根掉该功能的模拟类:

//Keep all the original functionality, but stub out the file delete functionality to 
//prevent it from using the real thing and while you're at it, keep a record that the
//method was called.
public class MockClass extends TheRealClass {
    boolean fileDeleteCalled = false;

    @Override
    protected void deleteFile()
        //don't actually delete the file, 
        //just record that the method to do so was called
        fileDeleteCalled = true;
    }

    public boolean fileDeleteCalled() { 
        return fileDeleteCalled; 
    }
}

最后,在您的单元测试中:

//This would normally be instantiated in the @Before method
private MockClass unitUnderTest = new MockClass();

@Test
public void testFileGetsDeleted(){
    assertFalse(unitUnderTest.fileDeleteCalled());
    unitUnderTest.someMethod();
    assertTrue(unitUnderTest.fileDeleteCalled());
}

现在你已经执行了someMethod()的所有功能,但实际上并没有删除文件,而且你仍然可以查看是否调用了该方法。


请有人给他更多的赞,因为他提供了很好的示例和解释。 - MSI

8
通常,如果方法满足以下条件:
  • 运行速度较慢
  • 执行时间较长
  • 包含复杂逻辑
  • 涉及文件系统操作
  • 连接到数据库
  • 调用Web服务
那么应避免将其设为静态方法。(有关原因和替代方案,请参见@adrianbanks的回答。)
基本上,只有当它是一个短小而方便的内存中方法(如许多扩展方法)时,才将其设置为静态方法。

我非常喜欢你的回答。它很有道理,你接受了测试静态逻辑作为SUT的一部分的影响,以便享受静态工具带来的便利,如果它没有你所指出的任何特征。因此,对于一个快速、简单、无副作用的静态方法,你假设它是一种基元,是你语言的扩展,并在你的单元中直接使用它,而不需要隔离。 - Didier A.

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