模仿静态方法

最近,我开始使用 莫克进行单元测试。我使用 Moq 模拟不需要测试的类。

您通常如何处理静态方法?

public void foo(string filePath)
{
File f = StaticClass.GetFile(filePath);
}

这个静态方法 StaticClass.GetFile()怎么会被模仿呢?

附注: 如果您能推荐任何关于 Moq 和单元测试的阅读材料,我将不胜感激。

106837 次浏览

Mocking frameworks like Moq or Rhinomocks can only create mock instances of objects, this means mocking static methods is not possible.

You can also search Google for more info.

Also, there's a few questions previously asked on StackOverflow here, here and here.

@Pure.Krome: good response but I will add a few details

@Kevin: You have to choose a solution depending on the changes that you can bring to the code.
If you can change it, some dependency injection make the code more testable. If you can't, you need a good isolation.
With free mocking framework (Moq, RhinoMocks, NMock...) you can only mock delegates, interfaces and virtual methods. So, for static, sealed and non-virtual methods you have 3 solutions:

  • TypeMock Isolator (can mock everything but it's expensive)
  • JustMock of Telerik (new comer, less expensive but still not free)
  • Moles of Microsoft (the only free solution for isolation)

I recommend Moles, because it's free, efficient and use lambda expressions like Moq. Just one important detail: Moles provide stubs, not mocks. So you may still use Moq for interface and delegates ;)

Mock: a class that implements an interface and allows the ability to dynamically set the values to return/exceptions to throw from particular methods and provides the ability to check if particular methods have been called/not called.
Stub: Like a mock class, except that it doesn't provide the ability to verify that methods have been called/not called.

There is possibility in .NET excluding MOQ and any other mocking library. You have to right click on solution explorer on assembly containing static method you want to mock and choose Add Fakes Assembly. Next you can freely mock that's assembly static methods.

Assume that you want to mock System.DateTime.Now static method. Do this for instance this way:

using (ShimsContext.Create())
{
System.Fakes.ShimDateTime.NowGet = () => new DateTime(1837, 1, 1);
Assert.AreEqual(DateTime.Now.Year, 1837);
}

You have similar property for each static property and method.

I've been playing around with a concept of refactoring the static methods to invoke a delegate which you can externally set for testing purposes.

This would not use any testing framework and would be a completely bespoke solution however the refactor will not influence the signature of your caller and so it would be a relatively safe.

For this to work, you would need to have access to the static method, so it wouldn't work for any external libraries such as System.DateTime.

Heres an example I've been playing with where I've created a couple of static methods, one with a return type that takes in two parameters and one generic which has no return type.

The main static class:

public static class LegacyStaticClass
{
// A static constructor sets up all the delegates so production keeps working as usual
static LegacyStaticClass()
{
ResetDelegates();
}


public static void ResetDelegates()
{
// All the logic that used to be in the body of the static method goes into the delegates instead.
ThrowMeDelegate = input => throw input;
SumDelegate = (a, b) => a + b;
}


public static Action<Exception> ThrowMeDelegate;
public static Func<int, int, int> SumDelegate;


public static void ThrowMe<TException>() where TException : Exception, new()
=> ThrowMeDelegate(new TException());


public static int Sum(int a, int b)
=> SumDelegate(a, b);
}

The Unit Tests (xUnit and Shouldly)

public class Class1Tests : IDisposable
{
[Fact]
public void ThrowMe_NoMocking_Throws()
{
Should.Throw<Exception>(() => LegacyStaticClass.ThrowMe<Exception>());
}


[Fact]
public void ThrowMe_EmptyMocking_DoesNotThrow()
{
LegacyStaticClass.ThrowMeDelegate = input => { };


LegacyStaticClass.ThrowMe<Exception>();


true.ShouldBeTrue();
}


[Fact]
public void Sum_NoMocking_AddsValues()
{
LegacyStaticClass.Sum(5, 6).ShouldBe(11);
}


[Fact]
public void Sum_MockingReturnValue_ReturnsMockedValue()
{
LegacyStaticClass.SumDelegate = (a, b) => 6;
LegacyStaticClass.Sum(5, 6).ShouldBe(6);
}


public void Dispose()
{
LegacyStaticClass.ResetDelegates();
}
}

You can achieve this with Pose library available from nuget. It allows you to mock, among other things, static methods. In your test method write this:

Shim shim = Shim.Replace(() => StaticClass.GetFile(Is.A<string>()))
.With((string name) => /*Here return your mocked value for test*/);
var sut = new Service();
PoseContext.Isolate(() =>
result = sut.foo("filename") /*Here the foo will take your mocked implementation of GetFile*/, shim);

For further reading refer here https://medium.com/@tonerdo/unit-testing-datetime-now-in-c-without-using-interfaces-978d372478e8

I liked Pose but couldn't get it to stop throwing InvalidProgramException which appears to be a known issue. Now I'm using Smocks like this:

Smock.Run(context =>
{
context.Setup(() => DateTime.Now).Returns(new DateTime(2000, 1, 1));


// Outputs "2000"
Console.WriteLine(DateTime.Now.Year);
});

Using Microsoft Fakes:

Add the fakes assembly, then if you have this static method...

//code under test
public static class MyClass {
public static int MyMethod() {
...
}
}

... you can mock it like this:

// unit test code
using (ShimsContext.Create())
{
ShimMyClass.MyMethod = () => 5;
}

Source: https://learn.microsoft.com/en-us/visualstudio/test/using-shims-to-isolate-your-application-from-other-assemblies-for-unit-testing?view=vs-2019#static-methods