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

发布于 2024-10-28 22:02:12 字数 2146 浏览 1 评论 0原文

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

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 种模式或实践来使调用静态函数的函数可进行单元测试。其中包括以下内容:

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

有时,为静态函数调用创建接口和包装类可能很麻烦,并且可能会因为许多小类(其唯一目的是调用静态函数)而污染您的解决方案。我完全赞成编写易于测试的代码,但这种做法似乎是针对不良测试框架的解决方法。

当我思考这些不同的解决方案时,我了解到上述所有 4 种做法都可以应用于不同的情况。我认为应用上述做法的正确做法是:

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

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

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:\test.txt"))
{
    string text = MyUtilities.GetFormattedText("hello world");
    MyUtilities.WriteTextToFile(text, fStream);
}

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.

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:

  1. Don't mock the static function at all and just let the unit test call it.
  2. 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.
  3. Use Moles (or TypeMock) to hijack the function call.
  4. 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:\test.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.

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

初见 2024-11-04 22:02: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,所以我不能过多谈论它。不过,我的印象和你的一样,如果你必须使用它,那么可能需要进行一些重构。

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.

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.

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:\test.txt")){
            string text = MyUtilities.GetFormattedText("hello world");
            MyUtilities.WriteTextToFile(text, fStream);
        }
    }
    catch(Exception e){
        //How do you test the code in here?
    }
}

With Dependency Injection:

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.
    }
}

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.

Without Dependency Injection:

This is a bit of a strange example/method but I am only using it to illustrate my point.

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

With Dependency Injection:

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

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.

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.

命硬 2024-11-04 22:02:12

欢迎来到静态的弊端。

我认为您的指导方针总体上还不错。以下是我的想法:

  • 对任何不会产生副作用的“纯函数”进行单元测试都可以,无论函数的可见性和范围如何。因此,对静态扩展方法(如“Linq 帮助程序”和内联字符串格式化(如 String.IsNullOrEmpty 或 String.Format 的包装器)和其他无状态实用函数)进行单元测试都很好。

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

  • 如果您只是必须实现真正的单例,请考虑将默认构造函数设置为受保护而不是完全私有,并定义一个从您的单例实例派生并允许在实例范围内创建对象的“测试代理”。这允许为任何产生副作用的方法生成“部分模拟”。

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

  • 只要代码引用静态(或具有其他副作用),并且无法重构为完全独立的类,将静态调用提取到方法中,并使用该类的“部分模拟”测试所有其他类功能,重写方法。

Welcome to the evils of static state.

I think your guidelines are OK, on the whole. Here are my thoughts:

  • Unit-testing any "pure function", which does not produce side effects, is fine regardless of the visibility and scope of the function. So, unit-testing static extension methods like "Linq helpers" and inline string formatting (like wrappers for String.IsNullOrEmpty or String.Format) and other stateless utility functions is all good.

  • Singletons are the enemy of good unit-testing. Instead of implementing the singleton pattern directly, consider registering the classes you want restricted to a single instance with an IoC container and injecting them to dependent classes. Same benefits, with the added benefit that IoC can be set up to return a mock in your testing projects.

  • If you simply must implement a true singleton, consider making the default constructor protected instead of fully private, and define a "test proxy" that derives from your singleton instance and allows for the creation of the object in instance scope. This allows for the generation of a "partial mock" for any methods that incur side effects.

  • If your code references built-in statics (such as ConfigurationManager) which are not fundamental to the operation of the class, either extract the static calls into a separate dependency which you can mock, or look for an instance-based solution. Obviously, any built-in statics are un-unit-testable, but there's no harm in using your unit-testing framework (MS, NUnit, etc) to build integration tests, just keep them separate so you can run unit tests without needing a custom environment.

  • Wherever code references statics (or has other side effects) and it is infeasible to refactor into a completely separate class, extract the static call into a method, and test all other class functionality using a "partial mock" of that class that overrides the method.

青春有你 2024-11-04 22:02:12

只需为静态方法创建一个单元测试,然后随意在方法内部调用它来进行测试,而无需模拟它。

Just create a unit test for static method and feel free to call it inside methods to test without mock it.

琉璃繁缕 2024-11-04 22:02:12

对于 File.CreateMyUtilities.WriteTextToFile,我将创建自己的包装器并通过依赖项注入来注入它。由于它涉及文件系统,因此该测试可能会因 I/O 而变慢,甚至可能会从文件系统引发一些意外的异常,这会让您认为您的类是错误的,但现在就是这样。

至于 MyUtilities.GetFormattedText 函数,我想这个函数只是对字符串做了一些更改,这里不用担心。

For the File.Create and MyUtilities.WriteTextToFile, I'd create my own wrapper and inject it with dependency injection. Since it touchs the FileSystem, this test could slow down because of the I/O and maybe even throw up some unexpected exception from the FileSystem which would lead you to think that your class is wrong, but it's now.

As for the MyUtilities.GetFormattedText function, I suppose this function only does some changes with the string, nothing to worry about here.

夏尔 2024-11-04 22:02:12

选择#1 是最好的。不要模拟,只使用存在的静态方法。这是最简单的路线,并且完全可以满足您的需要。您的两个“注入”场景仍在调用静态方法,因此您不会通过所有额外的包装获得任何东西。

Choice #1 is the best. Don't mock, and just use the static method as it exists. This is the simplest route and does exactly what you need it to do. Both of your 'injection' scenarios are still calling the static method, so you aren't gaining anything through all of the extra wrapping.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文