最近,我一直在思考“模拟”从我试图测试的类中调用的静态方法的最佳方法。以下面的代码为例:
using (FileStream fStream = File.Create(@"C:\test.txt"))
{
string text = MyUtilities.GetFormattedText("hello world");
MyUtilities.WriteTextToFile(text, fStream);
}
我知道这是一个相当糟糕的例子,但它有三个静态方法调用,它们略有不同。 File.Create函数访问文件系统,我没有该函数。 MyUtilities.GetFormattedText是我拥有的一个函数,它纯粹是无状态的。最后,MyUtilities.WriteTextToFile是我拥有的一个函数,它访问文件系统。
我最近一直在思考的是,如果这是遗留代码,我怎么能重构它以使其更具单元可测试性。我听过几个不应该使用静态函数的论点,因为它们很难测试。我不同意这个想法,因为静态函数是有用的,我不认为应该丢弃一个有用的工具只是因为正在使用的测试框架无法很好地处理它。
经过大量的搜索和审议,我得出的结论是,基本上可以使用 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);
}
}
}
有时,为静态函数调用创建接口和包装类可能很麻烦,并且它可能会污染您的解决方案,其中很多小类的唯一目的是调用静态函数。我只是编写易于测试的代码,但这种做法似乎是一个糟糕的测试框架的解决方法。
当我考虑这些不同的解决方案时,我开始理解上面提到的所有4种做法都可以应用于不同的情况。以下是我认为应用上述做法的正确的语言:
这些是我的想法,但我真的很感激对此的一些反馈。测试调用外部静态函数的代码的最佳方法是什么?
答案 0 :(得分:12)
使用依赖注入(选项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?
}
}
使用依赖注入:
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.
}
}
另一方面,如果无法读取/写入文件系统/数据库,您是否希望业务逻辑测试失败?如果我们在工资计算中测试数学是正确的,我们不希望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;
}
使用依赖注入:
public int GetNewSalary(IDBRepository aRepository,int aRaiseAmount){
//This call can now be mocked to always return something.
int oldSalary = aRepository.GetSalary();
return oldSalary + aRaiseAmount;
}
提高速度是一种额外的嘲弄。 IO成本高昂,IO的减少将提高测试速度。不必等待数据库事务或文件系统功能将提高您的测试性能。
我从来没有使用过TypeMock,所以我不能谈论它。我的印象与你的一样,如果你必须使用它,那么可能会有一些重构。
答案 1 :(得分:10)
欢迎来到静态的邪恶。
我认为你的指导方针总的来说还行。以下是我的想法:
无论功能的可见性和范围如何,单元测试任何不产生副作用的“纯函数”都很好。因此,单元测试静态扩展方法,如“Linq helpers”和内联字符串格式化(如String.IsNullOrEmpty或String.Format的包装)和其他无状态实用程序函数都很好。
单身人士是良好单位测试的敌人。不要直接实现单例模式,而是考虑使用IoC容器将您想要限制的类注册到单个实例,并将它们注入依赖类。同样的好处,可以设置IoC以在测试项目中返回模拟的额外好处。
如果您只是必须实现一个真正的单例,请考虑使默认构造函数受保护而不是完全私有,并定义一个派生自您的单例实例的“测试代理”,并允许在实例范围内创建该对象。这允许为任何产生副作用的方法生成“部分模拟”。
如果您的代码引用了不是该类操作基础的内置静态(例如ConfigurationManager),请将静态调用提取到可以模拟的单独依赖项中,或者查找实例 - 基于解决方案显然,任何内置静态都是不可单元测试的,但使用单元测试框架(MS,NUnit等)构建集成测试没有任何害处,只需将它们分开,这样就可以运行单元测试而无需自定义环境。
只要代码引用静态(或具有其他副作用)并且重构为完全独立的类是不可行的,将静态调用提取到方法中,并使用“部分模拟”来测试所有其他类功能那个覆盖方法的类。
答案 2 :(得分:1)
只需为静态方法创建一个单元测试,并随意在方法中调用它进行测试而不用模拟它。
答案 3 :(得分:1)
对于File.Create
和MyUtilities.WriteTextToFile
,我会创建自己的包装器并使用依赖注入注入它。由于它触及了FileSystem,这个测试可能因为I / O而变慢,甚至可能会从FileSystem中抛出一些意外的异常,这会导致你认为你的类是错的,但现在就是。
对于MyUtilities.GetFormattedText
函数,我想这个函数只对字符串做一些更改,这里不用担心。
答案 4 :(得分:-1)
选择#1是最好的。不要模拟,只使用存在的静态方法。这是最简单的路线,完全符合您的需要。你的两个“注入”场景仍在调用静态方法,所以你没有通过所有额外的包装获得任何东西。