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



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

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方法中,如下所示:

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方法以返回自己的测试数据。

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

原始代码:

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

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

class Math
{
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);
}

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

如其他答案所述,最小起订量不能模拟静态方法,作为一般规则,应尽可能避免静态方法。

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

缺点是,如果有很多代理,构造函数可能会变得非常混乱(尽管可以争辩说,如果有很多代理,那么该类可能会尝试做太多事情并且可以重构)

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

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 属性设置为我们的模拟。 使用这种样式可以减少要注入的接口数量,但会使依赖关系变得不那么明显(尽管我认为不会比原始静态调用更明显)。

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

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

这意味着更改以下内容:

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

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

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

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

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

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

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

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 delegate long AddFunction(int x, int y);
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);

。但这种语法很冗长,很难阅读。我不得不谷歌它。如果我们使用匿名函数而不是最小起订量,那就容易多了:

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

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


作为附带的一点,如果我们注入一个委托,我们如何用我们的 IoC 容器进行设置?它看起来就像注册接口和实现一样。使用IServiceCollection

serviceCollection.AddSingleton<AddFunction>(Arithemetic.Add);

遵循@manojlds的建议:

Moq(以及NMock,RhinoMock)在这里不会帮助你。您必须围绕 LogException 创建一个包装类(和虚拟方法),并在生产代码中使用它并使用它进行测试。

这是我想分享的部分解决方案。

我遇到了与此类似的问题,我实施了以下解决方案。

问题所在

具有静态方法的原始类

该类也可以是静态的。

public class LogHelper
{
public static string LogError(Exception ex, string controller, string method)
{
// Code
}
public static string LogInfo(string message, string controller, string method)
{
// Code
}
public static Logger Logger(string logId, string controller, string method)
{
// Code
}
}

你不能直接模拟这个,但你可以通过一些界面来模拟。

解决方案

界面

请注意,接口定义了该类中的所有静态方法。

public interface ILogHelperWrapper
{
string LogError(Exception ex, string controller, string method);
string LogInfo(string message, string controller, string method);
Logger Logger(string logId, string controller, string method);
}

然后,在类包装器中实现此接口。

包装类

public class LogHelperWrapper : ILogHelperWrapper
{
public string LogError(Exception ex, string controller, string method)
{
return LogHelper.LogError(ex, controller, method);            
}
public string LogInfo(string message, string controller, string method)
{
return LogHelper.LogInfo(message, controller, method);
}
public Logger Logger(string logId, string controller, string method)
{
return LogHelper.Logger(logId, controller, method);
}
}

这样你就可以模拟LogHelper的静态方法。

单元测试中的模拟

  • 这个接口是可模拟的,实现它的类可以在生产中使用(虽然我还没有在生产中运行它,但它在开发过程中有效)。
  • 然后,例如,我可以调用静态方法LogError
public void List_ReturnList_GetViewResultWithList()
{
// Arrange
var mockLogHelper = new Mock<ILogHelperWrapper>();

mockLogHelper.Setup(helper => helper.LogError(new Exception(), "Request", "List")).Returns("Some Returned value");
var controller = new RequestController(mockLogHelper.Object);
// Act
var actual = controller.DisplayList();
// Assert
Assert.IsType<ViewResult>(actual);
}

笔记

正如我之前所说,这是一个部分解决方案,我仍在实施。我正在将其检查为社区维基。

要精确地生成接口和实现,您可以尝试我的 lib YT。二根。

using YT.IIGen.Attributes;
[IIFor(typeof(StaticClass), "StaticClassWrapper")]
internal partial interface IStaticClass
{
}

最新更新