使用 Moq 模拟不安全的界面

本文关键字:界面 不安全 模拟 Moq 使用 | 更新日期: 2023-09-27 18:20:23

是否可以使用最小起订量来模拟不安全的界面?例如,我有 (MCVE(:

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public unsafe void TestMethod1()
    {
        Mock<IMyUnsafeInterface> mockDependency = new Mock<IMyUnsafeInterface>();
        mockDependency.Setup(i => i.DoWork(It.IsAny<int*>())); // error on this line
        systemUnderTest.Dependency = mockDependency.Object;
        ...
    }
}
public unsafe interface IMyUnsafeInterface
{
    void DoWork(int* intPtr);
    byte* MethodNotRelevantToThisTest1(byte* bytePtr);
    ComplexObject MethodNotRelevantToThisTest2();
    ...
}

但是我不能使用不安全的类型作为类型参数,并且出现错误:

Error   1   The type 'int*' may not be used as a type argument  

是否可以在不使用类型参数的情况下设置模拟以避免此问题?

(我知道显而易见的解决方案是不使用不安全的接口,我想知道是否有适用于不安全接口的解决方案。

编辑/更新:可以使用存根类,但如果可以使用 Moq,我想避免这种情况,因为 Moq 提供的代码要少得多。

public unsafe class MyUnsafeClass : IMyUnsafeInterface
{
    public void DoWork(int* intPtr) {
        // Do something
    }
    public byte* MethodNotRelevantToThisTest1(byte* bytePtr) {
        throw new NotImplementedException();
    }
    public ComplexObject MethodNotRelevantToThisTest2() {
        throw new NotImplementedException();
    }
    ...
}

使用 Moq 模拟不安全的界面

快速回答 如果您的类型在方法签名中具有指针类型,则不可能。


您看到的错误是因为不能将指针用作类型参数。 实际上,在 C# 规范中,您可以在第 4.4.1 节(类型参数(中找到:

在不安全的代码中,类型参数可能不是指针类型。

您可以通过将代码更改为需要特定指针来避免此特定错误:

Mock<IMyUnsafeInterface> mockDependency = new Mock<IMyUnsafeInterface>();
mockDependency.Setup(i => i.DoWork(any));
// use the mock
mockDependency.Object.DoWork(any);
mockDependency.Verify(p => p.DoWork(any));

但是,Moq 在Setup调用中失败,因为它尝试为参数创建一个Matcher对象(用于将设置与调用匹配(,该对象使用参数的类型作为类型参数。 这会导致与上述相同的错误。 传递通过Match.CreateIt.Is方法创建的自己的Match对象将不起作用,因为这些方法也采用类型参数。

如果省略Setup调用,利用松散的模拟行为,则 Moq 在Verify调用中失败,因为同样的问题。 它尝试根据参数参数的类型创建一个对象,以便它可以将记录的调用与传入的表达式匹配。

Moq 还提供了在提供 It 类之前匹配参数的较旧方法,您可以在其中使用 Matcher 属性标记方法:

[Test]
public unsafe void TestMethod1()
{
    int* any = stackalloc int[4];
    Mock<IMyUnsafeInterface> mockDependency = new Mock<IMyUnsafeInterface>();
    // use the mock
    mockDependency.Object.DoWork(any);
    mockDependency.Verify(p => p.DoWork(Is(any)));
}
[Matcher]
public static unsafe int* Is(int* expected) { return null; }
public static unsafe bool Is(int* actual, int* expected)
{
    return actual == expected;
}

这似乎可以工作,但失败并出现异常:

System.Security.VerificationException : 操作可能会破坏运行时的稳定性。   在lambda_method(关闭(   at Moq.MatcherFactory.CreateMatcher(Expression expression, Boolean isParams(   在Moq.MethodCall..ctor(Mock mock, Func'1 condition, Expression originalExpression, MethodInfo method, Expression[] 参数(   at Moq.Mock.Verify(Mock mock, Expression'1 expression, Times times, String failMessage(   at Moq.Mock'1.Verify(Expression'1 表达式(

我不太清楚这是从哪里来的,或者如何规避它,所以这也是一个死胡同。

这里最好的情况是,您可以将int*更改为 IntPtr ,这可以正常模拟。 如果您无法更改类型,那么根据您要验证的内容,您可以制作存根对象而不是依赖 Moq:

unsafe class StubMyUnsafeInterface : IMyUnsafeInterface
{
    readonly int* expectedIntPtr;
    public StubMyUnsafeInterface(int* expectedIntPtr)
    {
        this.expectedIntPtr = expectedIntPtr;
    }
    public unsafe void DoWork(int* intPtr)
    {
        Assert.That(intPtr == expectedIntPtr);
    }
}

然后在测试中:

int* any = stackalloc int[4];
int* other = stackalloc int[4];
var stubMyUnsafeInterface = new StubMyUnsafeInterface(any);
stubMyUnsafeInterface.DoWork(any);   // passes
stubMyUnsafeInterface.DoWork(other); // fails
下面

提供了一个代码示例,说明我通常如何解决此问题。 它涉及以下内容:

  1. 创建一个新接口(派生自有问题的接口(,该接口声明接受或返回非托管指针的每个方法的重载,并将其替换为 IntPtr(因为 Moq 可模拟IntPtr(。

    (注意:如果有一个方法仅在返回类型上有所不同,那么我只需稍微更改名称 - 例如我把Foo()改成FooIntPtr

  2. 使用类实现该新接口(该类在其构造函数中接受上面创建的新接口(。

    一个。对于此类中的每个有问题的方法(具有非托管指针的方法(,让它将调用委托给基于 IntPtr 的方法(通过执行适当的强制转换.
    b.对于所有其他方法,只需将它们转发到通过构造函数传入的接口即可。

  3. 在测试方法中:

    a. 使用 Moq 模拟新创建的界面版本。b.模拟方法的IntPtr版本.
    c. 从步骤 2 创建类的实例,并传入模拟。来自 3.a 的对象。d. 使用此实例传递到受测系统。

这样,您可以继续使用Moq来模拟所有方法。

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public unsafe void TestMethod1()
    {
        Mock<IMyUnsafeInterfaceForMoq> mockDependency = new Mock<IMyUnsafeInterfaceForMoq>();
        mockDependency.Setup(i => i.DoWork(It.IsAny<IntPtr>())); // use IntPtr instead of int*
        // Use this wrapper whenever passing it to the system under test:
        var dependency = new MyUnsafeInterfaceMoqWrapper(mockDependency.Object);
        var systemUnderTest = new SystemUnderTest();
        systemUnderTest.Dependency = dependency;
    }
}
public unsafe interface IMyUnsafeInterface
{
    void DoWork(int* intPtr);
    byte* MethodNotRelevantToThisTest1(byte* bytePtr);
    bool MethodNotRelevantToThisTest2();
}
// Interface to work around unsafe pointer types by using IntPtr
internal interface IMyUnsafeInterfaceForMoq : IMyUnsafeInterface
{
    void DoWork(IntPtr intPtr);
    IntPtr MethodNotRelevantToThisTest1(IntPtr bytePtr);
}
// Wrapper to workaround unsafe pointer types by using IntPtr
sealed class MyUnsafeInterfaceMoqWrapper : IMyUnsafeInterfaceForMoq
{
    readonly IMyUnsafeInterfaceForMoq _wrapped;
    public MyUnsafeInterfaceMoqWrapper(IMyUnsafeInterfaceForMoq wrapped)
    {
        _wrapped = wrapped ?? throw new ArgumentNullException(nameof(wrapped));
    }
    // For the methods that don't work with Moq, delegate to the methods that *do* work with moq
    // and cast appropriately.
    public unsafe void DoWork(int* intPtr) => DoWork((IntPtr)intPtr);
    public unsafe byte* MethodNotRelevantToThisTest1(byte* bytePtr) => (byte*)MethodNotRelevantToThisTest1((IntPtr)bytePtr);
    // The rest of the methods just forward on.
    public void DoWork(IntPtr intPtr) => _wrapped.DoWork(intPtr);
    public IntPtr MethodNotRelevantToThisTest1(IntPtr bytePtr) => _wrapped.MethodNotRelevantToThisTest1(bytePtr);
    public bool MethodNotRelevantToThisTest2() => _wrapped.MethodNotRelevantToThisTest2();
}
class SystemUnderTest
{
    public IMyUnsafeInterface Dependency { get; set; }
}

为了避免这种情况,你可以尝试使用包装类概念。您可以简单地包装原始类或接口。之后可以在 Wrapper 类函数中使用您的原始函数。就像下面的例子——

//Wrapper class
public class MyWrapperClass
{
     public YourReturnType MyFunction()
     {
         OriginalClass obj = new OriginalClass();
         //And inside this function call your original function
         return obj.OriginalFunction();
     }
}

Wrapper 类包装原始类型,我们可以根据需要使用/模拟原始对象。

如果您不知道包装类的概念,那么首先要了解它的概念。

http://www.c-sharpcorner.com/Blogs/12038/wrapper-class-in-C-Sharp.aspx