78

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

4

5 回答 5

125

Moq(和其他基于DynamicProxy的模拟框架)无法模拟任何不是虚拟或抽象方法的东西。

密封/静态类/方法只能使用基于 Profiler API 的工具来伪造,例如Typemock (商业)或 Microsoft Moles(免费,在 Visual Studio 2012 Ultimate /2013 /2015 中称为Fakes )。

或者,您可以重构您的设计以抽象对静态方法的调用,并通过依赖注入将此抽象提供给您的类。那么您不仅会有更好的设计,而且可以使用免费工具(如 Moq)进行测试。

无需完全使用任何工具即可应用允许可测试性的通用模式。考虑以下方法:

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

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

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

    protected virtual string[] GetDataFromFile(string fileName)
    {
        return FileUtil.ReadDataFromFile(fileName);
    }
}

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

希望有帮助。

于 2012-09-25T09:32:59.950 回答
63

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

原始代码:

    class Math
    {
        public static int Add(int x, int y)
        {
            return x + y;
        }

您想“模拟” Add 方法,但不能。把上面的代码改成这样:

        public static Func<int, int, int> Add = (x, y) =>
        {
            return x + y;
        };

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

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

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

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

这可能不一定是您每次都可以做的事情,但在实践中,我发现这种技术效果很好。

[编辑] 我建议您将以下清理代码添加到您的单元测试类:

    [TestCleanup]
    public void Cleanup()
    {
        typeof(Math).TypeInitializer.Invoke(null, null);
    }

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

于 2015-02-19T23:17:14.097 回答
8

Moq 不能模拟类的静态成员。

在为可测试性设计代码时,避免静态成员(和单例)很重要。一种可以帮助您重构代码以提高可测试性的设计模式是依赖注入。

这意味着改变这一点:

public class Foo
{
    public Foo()
    {
        Bar = new Bar();
    }
}

public Foo(IBar bar)
{
    Bar = bar;
}

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

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

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

于 2012-09-25T09:36:57.097 回答
8

正如其他答案中提到的那样,MOQ 不能模拟静态方法,作为一般规则,应该尽可能避免使用静态方法。

有时这是不可能的。一种是使用遗留代码或第 3 方代码,甚至使用静态的 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);

        }
    }

不利的一面是,如果有很多代理,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);
        }

    }

我们的方法现在变成

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

    }

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

于 2012-09-28T10:10:43.377 回答
0

我们通常通过依赖于接口之类的抽象来模拟实例(非静态)类及其方法,而不是直接依赖于具体类。

我们可以对静态方法做同样的事情。这是一个依赖于静态方法的类的示例。(这是非常人为的。)在这个例子中,我们直接依赖于静态方法,所以我们不能模拟它。

public class DoesSomething
{
    public long AddNumbers(int x, int y)
    {
        return Arithemetic.Add(x, y); // We can't mock this :(
    }
}

public static class Arithemetic
{
    public static long Add(int x, int y) => x + y;
}

为了能够模拟该Add方法,我们可以注入一个抽象。Func<int, int, long>我们可以注入一个或一个委托,而不是注入一个接口。两者都可以,但我更喜欢委托,因为我们可以给它起一个名称,说明它的用途,并将它与具有相同签名的其他函数区分开来。

这是委托以及注入委托时类的外观:


public class DoesSomething
{
    private readonly AddFunction _addFunction;

    public DoesSomething(AddFunction addFunction)
    {
        _addFunction = addFunction;
    }

    public long AddNumbers(int x, int y)
    {
        return _addFunction(x, y);
    }
}

这与我们将接口注入类的构造函数时的工作方式完全相同。

我们可以使用 Moq 为委托创建模拟,就像我们使用接口一样。

var addFunctionMock = new Mock<AddFunction>();
addFunctionMock.Setup(_ => _(It.IsAny<int>(), It.IsAny<int>())).Returns(2);
var sut = new DoesSomething(addFunctionMock.Object);

...但是这种语法令人费解。我不得不谷歌它。如果我们使用匿名函数而不是 Moq 会容易得多:

AddFunction addFunctionMock = (x, y) => 2;
var sut = new DoesSomething(addFunctionMock);

我们可以使用任何具有正确签名的方法。如果我们愿意,我们可以使用该签名在我们的测试类中定义另一个方法并使用它。


顺便说一句,如果我们注入一个委托,我们如何使用我们的 IoC 容器进行设置?它看起来就像注册一个接口和实现。使用IServiceCollection

serviceCollection.AddSingleton<AddFunction>(Arithemetic.Add);
于 2021-12-21T18:49:14.277 回答