调用静态方法的单元测试方法的模式或实践

Jac*_*ams 34 c# static unit-testing function mocking

最近,我一直在思考"模拟"从我试图测试的类中调用的静态方法的最佳方法.以下面的代码为例:

using (FileStream fStream = File.Create(@"C:\test.txt"))
{
    string text = MyUtilities.GetFormattedText("hello world");
    MyUtilities.WriteTextToFile(text, fStream);
}
Run Code Online (Sandbox Code Playgroud)

我知道这是一个相当糟糕的例子,但它有三个静态方法调用,它们略有不同.File.Create函数访问文件系统,我没有该函数.MyUtilities.GetFormattedText是我拥有的一个函数,它纯粹是无状态的.最后,MyUtilities.WriteTextToFile是我拥有的一个函数,它访问文件系统.

我最近一直在思考的是,如果这是遗留代码,我怎么能重构它以使它更可单元测试.我听过几个不应该使用静态函数的论点,因为它们很难测试.我不同意这个想法,因为静态函数是有用的,我不认为应该丢弃一个有用的工具只是因为正在使用的测试框架无法很好地处理它.

经过大量的搜索和审议,我得出的结论是,基本上可以使用4种模式或实践来使函数调用静态函数可单元测试.这些包括以下内容:

  1. 根本不要模拟静态函数,只需让单元测试它.
  2. 将静态方法包装在实例类中,该实例类使用您需要的函数实现接口,然后使用依赖注入在类中使用它.我将其称为接口依赖注入.
  3. 使用Moles(或TypeMock)来劫持函数调用.
  4. 对函数使用依赖注入.我将其称为函数依赖注入.

我听过很多关于前三种做法的讨论,但是当我在考虑这个问题的解决方案时,我想到了函数依赖注入的第四个想法.这类似于在接口后面隐藏静态函数,但实际上不需要创建接口和包装类.这方面的一个例子如下:

public class MyInstanceClass
{
    private Action<string, FileStream> writeFunction = delegate { };

    public MyInstanceClass(Action<string, FileStream> functionDependency)
    {
        writeFunction = functionDependency;
    }

    public void DoSomething2()
    {
        using (FileStream fStream = File.Create(@"C:\test.txt"))
        {
            string text = MyUtilities.GetFormattedText("hello world");
            writeFunction(text, fStream);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

有时,为静态函数调用创建接口和包装类可能很麻烦,并且它可能会污染您的解决方案,其中许多小类的唯一目的是调用静态函数.我只是编写易于测试的代码,但这种做法似乎是一个糟糕的测试框架的解决方法.

当我考虑这些不同的解决方案时,我开始理解上面提到的所有4种做法都可以应用于不同的情况.以下是我认为应用上述实践正确条件:

  1. 如果静态函数纯粹是无状态的并且不访问系统资源(例如文件系统或数据库),请不要模拟静态函数.当然,可以进行论证,如果正在访问系统资源,那么无论如何这都会将状态引入静态函数.
  2. 当您使用的多个静态函数可以在逻辑上将所有静态函数添加到单个接口时,请使用接口依赖性注入.这里的关键是使用了几个静态函数.我认为在大多数情况下情况并非如此.在函数中可能只会调用一个或两个静态函数.
  3. 在模拟外部库(如UI库或数据库库(例如linq to sql))时使用Moles.我的观点是,如果使用Moles(或TypeMock)来劫持CLR以模拟自己的代码,那么这表明需要进行一些重构来解耦对象.
  4. 当正在测试的代码中存在少量静态函数调用时,请使用函数依赖项注入.这是我在大多数情况下倾向于的模式,以便测试在我自己的实用程序类中调用静态函数的函数.

这些是我的想法,但我真的很感激对此的一些反馈.测试调用外部静态函数的代码的最佳方法是什么?

bra*_*mus 16

使用依赖注入(选项2或4)绝对是我攻击它的首选方法.它不仅使测试更容易,而且有助于分离问题并防止类变得臃肿.

我需要澄清的是,静态方法难以测试是不正确的.当静态方法用于另一种方法时,会出现问题.这使得调用静态方法的方法难以测试,因为无法模拟静态方法.通常的例子是I/O. 在您的示例中,您正在将文本写入文件(WriteTextToFile).如果在这种方法中出现问题怎么办?由于该方法是静态的并且无法模拟,因此您无法按需创建诸如故障情况之类的情况.如果您创建了一个接口,那么您可以模拟对WriteTextToFile的调用并让它模拟错误.是的,你会有更多的接口和类,但通常你可以在一个类中逻辑地将类似的功能组合在一起.

没有依赖注入: 这几乎是选项1,没有任何东西被嘲笑.我不认为这是一个可靠的策略,因为它不允许你进行彻底的测试.

public void WriteMyFile(){
    try{
        using (FileStream fStream = File.Create(@"C:\test.txt")){
            string text = MyUtilities.GetFormattedText("hello world");
            MyUtilities.WriteTextToFile(text, fStream);
        }
    }
    catch(Exception e){
        //How do you test the code in here?
    }
}
Run Code Online (Sandbox Code Playgroud)

使用依赖注入:

public void WriteMyFile(IFileRepository aRepository){
    try{
        using (FileStream fStream = aRepository.Create(@"C:\test.txt")){
            string text = MyUtilities.GetFormattedText("hello world");
            aRepository.WriteTextToFile(text, fStream);
        }
    }
    catch(Exception e){
        //You can now mock Create or WriteTextToFile and have it throw an exception to test this code.
    }
}
Run Code Online (Sandbox Code Playgroud)

另一方面,如果无法读取/写入文件系统/数据库,您是否希望业务逻辑测试失败?如果我们在工资计算中测试数学是正确的,我们不希望IO错误导致测试失败.

没有依赖注入:

这是一个奇怪的例子/方法,但我只是用它来说明我的观点.

public int GetNewSalary(int aRaiseAmount){
    //Do you really want the test of this method to fail because the database couldn't be queried?
    int oldSalary = DBUtilities.GetSalary(); 
    return oldSalary + aRaiseAmount;
}
Run Code Online (Sandbox Code Playgroud)

使用依赖注入:

public int GetNewSalary(IDBRepository aRepository,int aRaiseAmount){
    //This call can now be mocked to always return something.
    int oldSalary = aRepository.GetSalary();
    return oldSalary + aRaiseAmount;
}
Run Code Online (Sandbox Code Playgroud)

提高速度是一种额外的嘲弄.IO成本高昂,IO的减少将提高测试速度.无需等待数据库事务或文件系统功能即可提高测试性能.

我从来没有使用过TypeMock,所以我不能谈论它.我的印象与你的一样,如果你必须使用它,那么可能会有一些重构.


Kei*_*thS 13

欢迎来到静态的邪恶.

总的来说,我认为你的指导方针是可以的.这是我的想法:

  • 无论功能的可见性和范围如何,单元测试任何不产生副作用的"纯函数"都很好.因此,单元测试静态扩展方法,如"Linq helpers"和内联字符串格式(如String.IsNullOrEmpty或String.Format的包装)和其他无状态实用程序函数都很好.

  • 单身人士是良好单位测试的敌人.不要直接实现单例模式,而是考虑使用IoC容器将您想要限制的类注册到单个实例,并将它们注入依赖类.同样的好处,可以设置IoC以在测试项目中返回模拟的额外好处.

  • 如果您只需要实现一个真正的单例,请考虑使默认构造函数受保护而不是完全私有,并定义一个派生自您的单例实例的"测试代理",并允许在实例范围内创建该对象.这允许为任何产生副作用的方法产生"部分模拟".

  • 如果您的代码引用了内部静态(例如ConfigurationManager),这些静态调用对于类的操作不是基础,则将静态调用提取到可以模拟的单独依赖项中,或者查找基于实例的解决方案.显然,任何内置静态都是不可单元测试的,但使用单元测试框架(MS,NUnit等)构建集成测试没有任何害处,只需将它们分开,这样就可以运行单元测试而无需自定义环境.

  • 只要代码引用静态(或具有其他副作用)并且重构为完全独立的类是不可行的,将静态调用提取到方法中,并使用覆盖该方法的该类的"部分模拟"来测试所有其他类功能.

  • 我喜欢将静态调用移动到可重写方法的建议.它基本上是Michael Feathers提到的"提取和覆盖"技术(有效地使用Legacy Code_)和Roy Osherove(_The Unit of Unit Testing_).感谢您对该技术慢慢记忆! (3认同)