如何使用MOQ框架在c#中模拟静态方法?

Him*_*shu 51 c# unit-testing moq mocking

我最近一直在进行单元测试,并且使用MOQ框架和MS Test成功地模拟了各种场景.我知道我们不能测试私有方法,但我想知道我们是否可以使用MOQ模拟静态方法.

Iga*_*nik 94

MOQ(和其他DynamicProxy基础的嘲笑框架)无法嘲笑任何不是虚拟或抽象方法.

密封/静态类/方法只能与基于探查API工具伪造,像Typemock(商业)或Microsoft痣(免费,被称为假货在Visual Studio 2012旗舰版/二千零十五分之二千零十三).

或者,您可以重构您的设计以抽象对静态方法的调用,并通过依赖注入将此抽象提供给您的类.那么你不仅要有更好的设计,还可以使用免费工具测试,比如Moq.

可以在不使用任何工具的情况下应用允许可测试性的常见模式.请考虑以下方法:

public class MyClass
{
    public string[] GetMyData(string fileName)
    {
        string[] data = FileUtil.ReadDataFromFile(fileName);
        return data;
    }
}
Run Code Online (Sandbox Code Playgroud)

FileUtil.ReadDataFromFile您可以将其包装在protected virtual方法中,而不是尝试模拟,如下所示:

public class MyClass
{
    public string[] GetMyData(string fileName)
    {
        string[] data = GetDataFromFile(fileName);
        return data;
    }

    protected virtual string[] GetDataFromFile(string fileName)
    {
        return FileUtil.ReadDataFromFile(fileName);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,在您的单元测试中,派生MyClass并调用它TestableMyClass.然后,您可以覆盖该GetDataFromFile方法以返回您自己的测试数据.

希望有所帮助.

  • @Igal你会如何测试受保护的虚拟字符串[] GetDataFromFile(string fileName)方法?你会不确定这种方法吗? (2认同)

zum*_*ard 43

将静态方法转换为静态Func或Action的另一种选择.例如.

原始代码:

    class Math
    {
        public static int Add(int x, int y)
        {
            return x + y;
        }
Run Code Online (Sandbox Code Playgroud)

你想"模拟"Add方法,但你不能.将上面的代码更改为:

        public static Func<int, int, int> Add = (x, y) =>
        {
            return x + y;
        };
Run Code Online (Sandbox Code Playgroud)

现有客户端代码不必更改(可能重新编译),但源保持不变.

现在,从单元测试来改变方法的行为,只需重新分配一个内联函数:

    [TestMethod]
    public static void MyTest()
    {
        Math.Add = (x, y) =>
        {
            return 11;
        };
Run Code Online (Sandbox Code Playgroud)

在方法中放置您想要的任何逻辑,或者只返回一些硬编码值,具体取决于您要执行的操作.

这可能不一定是你每次都可以做的事情,但在实践中,我发现这种技术运作得很好.

[edit]我建议您将以下清理代码添加到Unit Test类:

    [TestCleanup]
    public void Cleanup()
    {
        typeof(Math).TypeInitializer.Invoke(null, null);
    }
Run Code Online (Sandbox Code Playgroud)

为每个静态类添加单独的行.这样做是在单元测试运行完毕后,将所有静态字段重置为原始值.这样,同一项目中的其他单元测试将以正确的默认值开始,而不是您的模拟版本.

  • 请注意,任何消费类都可以覆盖此"方法",而不仅仅是您的单元测试!为什么这很重要?`public static Func <string,bool> IsSecredPasswordValid ....`可以很容易地重写,总是返回`true`.稍微好一点的安全性至少是将`Func`声明为`internal`并使用`InternalsVisibleTo`属性来仅允许你的单元测试. (8认同)
  • 另外,要记住另一件事.当你设置这样的静态变量时,你应该在测试完成后撤消它.一种方法是重新运行类的静态初始化程序.示例:typeof(SomeClassName).TypeInitializer.Invoke(null,null); (3认同)
  • 这是迄今为止我最喜欢的方法.将所有内容放入非静态类只是为了让你可以模拟这个方法对我来说是一个巨大的代码味道.特别是当您最终得到这样的代码时:`var people = new PersonRepository().GetAll();`.创建PersonRepository在这里没有用处. (3认同)
  • 并行运行测试时更改静态属性可能会导致问题。 (3认同)
  • 这太棒了。非常感谢。如此之少的变化,以及如此巨大的收益。我想要抱你。 (2认同)

Ala*_*anT 8

正如其他答案中所提到的,MOQ不能模拟静态方法,并且作为一般规则,应尽可能避免静态.

有时这是不可能的.一种是使用遗留代码或第三方代码,或者使用静态的BCL方法.

一种可能的解决方案是将静态包装在具有可以模拟的接口的代理中

    public interface IFileProxy {
        void Delete(string path);
    }

    public class FileProxy : IFileProxy {
        public void Delete(string path) {
            System.IO.File.Delete(path);
        }
    }

    public class MyClass {

        private IFileProxy _fileProxy;

        public MyClass(IFileProxy fileProxy) {
            _fileProxy = fileProxy;
        }

        public void DoSomethingAndDeleteFile(string path) {
            // Do Something with file
            // ...
            // Delete
            System.IO.File.Delete(path);
        }

        public void DoSomethingAndDeleteFileUsingProxy(string path) {
            // Do Something with file
            // ...
            // Delete
            _fileProxy.Delete(path);

        }
    }
Run Code Online (Sandbox Code Playgroud)

缺点是,如果有很多代理,ctor可能会变得非常混乱(虽然可以说如果有很多代理,那么这个类可能会尝试做太多而且可能会被重构)

另一种可能性是拥有一个"静态代理",其背后有不同的接口实现

   public static class FileServices {

        static FileServices() {
            Reset();
        }

        internal static IFileProxy FileProxy { private get; set; }

        public static void Reset(){
           FileProxy = new FileProxy();
        }

        public static void Delete(string path) {
            FileProxy.Delete(path);
        }

    }
Run Code Online (Sandbox Code Playgroud)

我们的方法现在成了

    public void DoSomethingAndDeleteFileUsingStaticProxy(string path) {
            // Do Something with file
            // ...
            // Delete
            FileServices.Delete(path);

    }
Run Code Online (Sandbox Code Playgroud)

为了测试,我们可以将FileProxy属性设置为mock.使用这种风格减少了要注入的接口数量,但使依赖关系变得不那么明显(尽管不超过我想的原始静态调用).


Wou*_*ort 6

Moq无法模拟类的静态成员.

在设计可测试性代码时,避免使用静态成员(和单例)非常重要.可以帮助您重构代码以实现可测试性的设计模式是依赖注入.

这意味着改变这个:

public class Foo
{
    public Foo()
    {
        Bar = new Bar();
    }
}
Run Code Online (Sandbox Code Playgroud)

public Foo(IBar bar)
{
    Bar = bar;
}
Run Code Online (Sandbox Code Playgroud)

这允许您使用单元测试中的模拟.在生产中,您可以使用像NinjectUnity这样的依赖注入工具,它可以将所有内容连接在一起.

不久前我写了一篇关于这个的博客.它解释了哪些模式可用于更好的可测试代码.也许它可以帮助你:单元测试,地狱还是天堂?

另一种解决方案可能是使用Microsoft Fakes Framework.这不是写好设计的可测试代码的替代品,但它可以帮助你.Fakes框架允许您模拟静态成员并在运行时使用您自己的自定义行为替换它们.