问题描述
最近,我一直在认真思考模拟"从我要测试的类中调用的静态方法的最佳方法.以如下代码为例:
As of late, I have been pondering heavily about the best way to "Mock" a static method that is called from a class that I am trying to test. Take the following code for example:
using (FileStream fStream = File.Create(@"C: est.txt"))
{
string text = MyUtilities.GetFormattedText("hello world");
MyUtilities.WriteTextToFile(text, fStream);
}
我知道这是一个相当糟糕的例子,但它有三个静态方法调用,它们都略有不同.File.Create 函数访问文件系统,我不拥有该函数.MyUtilities.GetFormattedText 是我拥有的一个函数,它完全是无状态的.最后,MyUtilities.WriteTextToFile 是我拥有的一个函数,它访问文件系统.
I understand that this is a rather bad example, but it has three static method calls that are all different slightly. The File.Create function access the file system and I don't own that function. The MyUtilities.GetFormattedText is a function that I own and it is purely stateless. Finally, the MyUtilities.WriteTextToFile is a function I own and it accesses the file system.
我最近一直在思考的是,如果这是遗留代码,我该如何重构它以使其更易于单元测试.我听过几个论点,认为不应该使用静态函数,因为它们很难测试.我不同意这个想法,因为静态函数很有用,我不认为应该因为正在使用的测试框架不能很好地处理它而丢弃有用的工具.
What I have been pondering lately is if this were legacy code, how could I refactor it to make it more unit-testable. I have heard several arguments that static functions should not be used because they are hard to test. I disagree with this idea because static functions are useful and I don't think that a useful tool should be discarded just because the test framework that is being used can't handle it very well.
经过大量搜索和深思熟虑,我得出的结论是,基本上有 4 种模式或实践可用于使调用静态函数的函数可单元测试.其中包括:
After much searching and deliberation, I have come to the conclusion that there are basically 4 patterns or practices that can be used in order to make functions that call static functions unit-testable. These include the following:
- 不要模拟静态函数,让单元测试调用它.
- 将静态方法封装在一个实例类中,该实例类实现了一个带有您需要的函数的接口,然后使用依赖注入在您的类中使用它.我将其称为接口依赖注入.
- 使用 Moles(或 TypeMock)劫持函数调用.
- 对函数使用依赖注入.我将其称为函数依赖注入.
- Don't mock the static function at all and just let the unit test call it.
- Wrap the static method in an instance class that implements an interface with the function that you need on it and then use dependency injection to use it in your class. I'll refer to this as interface dependency injection.
- Use Moles (or TypeMock) to hijack the function call.
- Use dependeny injection for the function. I'll refer to this as function dependency injection.
我听过很多关于前三种做法的讨论,但是当我在思考这个问题的解决方案时,我想到了函数依赖注入.这类似于将静态函数隐藏在接口后面,但实际上不需要创建接口和包装类.这方面的一个例子如下:
I've heard quite a lot of discussion about the first three practices, but as I was thinking about solutions to this problem, the forth idea came to me of function dependency injection. This is similar to hiding a static function behind an interface, but without actually needing to create an interface and wrapper class. An example of this would be the following:
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: est.txt"))
{
string text = MyUtilities.GetFormattedText("hello world");
writeFunction(text, fStream);
}
}
}
有时,为静态函数调用创建接口和包装类可能会很麻烦,并且可能会使用许多仅用于调用静态函数的小类来污染您的解决方案.我完全赞成编写易于测试的代码,但这种做法似乎是一个糟糕的测试框架的解决方法.
Sometimes, creating an interface and wrapper class for a static function call can be cumbersome and it can pollute your solution with a lot of small classes whose sole purpose is to call a static function. I am all for writing code that is easily testable, but this practice seems to be a workaround for a bad testing framework.
在考虑这些不同的解决方案时,我了解到上述所有 4 种做法都可以应用于不同的情况.以下是我认为应用上述做法的正确情况:
As I was thinking about these different solutions, I came to an understanding that all of the 4 practices mentioned above can be applied in different situations. Here is what I am thinking is the correct cicumstances to apply the above practices:
- 不要模拟静态函数,如果它是纯无状态的并且不访问系统资源(例如文件系统或数据库).当然,可以这样说,如果正在访问系统资源,那么无论如何这都会将状态引入静态函数.
- 使用接口依赖注入 当您使用的多个静态函数都可以在逻辑上添加到单个接口时.这里的关键是使用了几个静态函数.我认为在大多数情况下,情况并非如此.一个函数中可能只会调用一两个静态函数.
- 在模拟 UI 库或数据库库(如 linq to sql)等外部库时使用 Moles.我的观点是,如果 Moles(或 TypeMock)被用来劫持 CLR 以模拟您自己的代码,那么这表明需要进行一些重构以解耦对象.
- 当正在测试的代码中有少量静态函数调用时,使用函数依赖注入.这是我在大多数情况下倾向于使用的模式,以便测试在我自己的实用程序类中调用静态函数的函数.
- Don't mock the static function if it is purely stateless and does not access system resources (such as the filesystem or a database). Of course, the argument can be made that if system resources are being accessed then this introduces state into the static function anyway.
- Use interface dependency injection when there are several static functions that you are using that can all logically be added to a single interface. The key here is that there are several static functions being used. I think that in most cases this will not be the case. There will probably only be one or two static functions being called in a function.
- Use Moles when you are mocking up external libraries such as UI libraries or database libraries (such as linq to sql). My opinion is that if Moles (or TypeMock) is used to hijack the CLR in order to mock your own code, then this is an indicator that some refactoring needs to be done to decouple the objects.
- Use function dependency injection when there is a small number of static function calls in the code that is being tested. This is the pattern that I am leaning towards in most cases in order to test functions that are calling static functions in my own utility classes.
这些是我的想法,但我非常感谢对此的一些反馈.测试调用外部静态函数的代码的最佳方法是什么?
These are my thoughts, but I would really appreciate some feedback on this. What is the best way to test code where an external static function is being called?
推荐答案
使用依赖注入(选项 2 或 4)绝对是我首选的攻击方法.它不仅使测试更容易,还有助于分离关注点并防止类变得臃肿.
Using dependency injection (either option 2 or 4) is definitely my preferred method of attacking this. Not only does it make testing easier it helps to separate concerns and keep classes from getting bloated.
我需要澄清一下,静态方法很难测试是不正确的.当静态方法用于另一种方法时,就会出现问题.这使得调用静态方法的方法难以测试,因为无法模拟静态方法.常见的例子是 I/O.在您的示例中,您正在将文本写入文件(WriteTextToFile).如果在此方法中出现故障怎么办?由于该方法是静态的并且无法模拟,因此您无法按需创建失败案例等案例.如果您创建一个接口,那么您可以模拟对 WriteTextToFile 的调用并让它模拟错误.是的,您将拥有更多的接口和类,但通常您可以将类似的功能按逻辑分组到一个类中.
A clarification I need to make though is it is not true that static methods are hard to test. The problem with static methods occurs when they are used in another method. This makes the method that is calling the static method hard to test as the static method can not be mocked. The usual example of this is with I/O. In your example you are writing text to a file (WriteTextToFile). What if something should fail during this method? Since the method is static and it can't be mocked then you can't on demand create cases such as failure cases. If you create an interface then you can mock the call to WriteTextToFile and have it mock errors. Yes you'll have a few more interfaces and classes but normally you can group similar functions together logically in one class.
没有依赖注入:这几乎是选项 1,没有任何东西被嘲笑.我不认为这是一个可靠的策略,因为它不允许您进行彻底的测试.
Without Dependency Injection: This is pretty much option 1 where nothing is mocked. I don't see this as a solid strategy because it does not allow you to thoroughly test.
public void WriteMyFile(){
try{
using (FileStream fStream = File.Create(@"C: est.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: est.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 错误导致测试失败.
On the flip side of this is do you want your business logic tests to fail if the file system/database can't be read/written to? If we're testing that the math is correct in our salary calculation we don't want IO errors to cause the test to fail.
没有依赖注入:
这是一个有点奇怪的例子/方法,但我只是用它来说明我的观点.
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 会提高测试速度.不必等待数据库事务或文件系统功能将提高您的测试性能.
Increased speed is an additional perk of mocking. IO is costly and reduction in IO will increase the speed of your tests. Not having to wait for a database transaction or file system function will improve your tests performance.
我从来没有使用过 TypeMock,所以我不能多说.不过我的印象和你一样,如果你必须使用它,那么可能需要进行一些重构.
I've never used TypeMock so I can't speak much about it. My impression though is the same as yours that if you have to use it then there is probably some refactoring that could be done.
这篇关于调用静态方法的单元测试方法的模式或实践的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持跟版网!