NativeMock 0.2.1

dotnet add package NativeMock --version 0.2.1                
NuGet\Install-Package NativeMock -Version 0.2.1                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NativeMock" Version="0.2.1" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add NativeMock --version 0.2.1                
#r "nuget: NativeMock, 0.2.1"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install NativeMock as a Cake Addin
#addin nuget:?package=NativeMock&version=0.2.1

// Install NativeMock as a Cake Tool
#tool nuget:?package=NativeMock&version=0.2.1                

NativeMock

A .NET 5/.NET Framework 4.6.2 library that allows you to mock P/Invoke calls simply by defining an interface for it.

Testing code with P/Invoke calls are usually difficult to test in unit test scenarios. Either you need to use and load the dependent native library or create a custom test-only library. Regardless of solution it makes testing code with P/Invoke calls harder. This is where NativeMock aims to assist.

With NativeMock it is possible to redirect P/Invoke calls to a custom defined interface. Mocking P/Invoke calls is as simple as defining an interface and providing an implementation. The implementation could be hand-written or come from a mocking framework of your choice.

Example

The following example shows how the P/Invoke function MyExternalFunction can be mocked using NativeMock. Please note that a one-time initialization is necessary but has been excluded from the example for simplicity (see installation below):

[DllImport("test.dll")]
public extern int MyExternalFunction([MarshalAs (UnmanagedType.LPUTF8Str)] string s);

// 1. Define an interface that will receive the calls instead
[NativeMockInterface ("test.dll")]
public interface INativeLibraryApi
{
  // 2. Define a method with a matching name and signature
  int MyExternalFunction([MarshalAs(UnmanagedType.LPUTF8Str)] string s);
}

[Test]
public void MyExternalFunctionTest()
{
  var mock = new Mock<T>(MockBehavior.Strict);
  mock.Setup(e => e.MyExternalFunction("Test")).Return(5);
    
  // 3. Setup an implementation that receives the calls
  NativeMockRepository.Setup(mock.Object);

  // 4. Call the P/Invoke function
  Assert.That(MyExternalFunction("Test"), Is.EqualTo(5));
  mock.VerifyAll();
}

Features

  • Mock any P/Invoke function simply by declaring an interface
    • Multiple P/Invoke function per interface
    • Different mock behaviors when there is no setup (Strict, Loose, and Forward. see NativeMockBehavior)
    • Change the name of the interface method by applying the NativeMockCallback attribute
  • Load stand-in native libraries to prevent the actual native libraries from being called (see NativeLibraryDummy.Load)
  • Analyzers to prevent signature mismatches between a P/Invoke function and their mock counterpart in the interface (rules: NMxxxx)
    • Requires setting a DeclaringType via attribute on the interface or a certain interface method
  • Basic mocking framework for scenarios that are not well supported in other mocking frameworks (see .Setup & .SetupAlternate)
    • Most of the time, the setup is done using an Expression<T> which does not work for certain types, for example, stack-only types.
    • Setup requires delegate types to setup but that can be generated by NativeMock via the code generator (make interface partial)

Limitations

  • Windows only: Currently, the library only supports windows as the import address table (IAT) hook is only implemented for windows.

  • Initialization necessary: To intercept calls using IAT hooks the initialization must be done before any of the native methods are called. Any P/Invoke call that was called before the initialization will work normally but cannot be mocked.

  • Mocks cannot be destroyed: With the way P/Invoke methods are dynamically generated by the CLR it is not possible to undo mocks. Once they are setup they cannot be destroyed and last until the process exits. Use NativeMockBehavior.Forward if you want to call the original method when no mock is set up.

  • Not all functions should be mocked: While there is no technical restriction to what P/Invoke calls you can mock or not there are some that you should not mock. Specifically: Do not mock any P/Invoke methods that might be called by the GC during a collection. Refrain from mocking functions from shared libraries (e.g. Kernel32, ...). Trust me that is not a fun thing to debug as your process will hang and no longer respond to the debugger 😃

    • Fun fact: It is not actually the hook that causes the problem. Your callback gets called during a GC without problems. The problem occurs when your callback tries to allocate something. Seems like the GC does not like allocations while it is doing a GC 🤷‍♂️
  • The method signatures + their attributes must align: If the P/Invoke function and their interface method counterpart differ in their signature or even their marshalling attributes weird behavior/crashes might occur as parameters/return values are passed incorrectly. Make sure the signatures match. The analyzer should help you with that by reporting diagnostics if you use the DeclaringType attribute property.

Installation

Installation via the Nuget package is preferred as it also setups the analyzer/code generator as well as the required native components.

Due to technical limitations it is necessary to initialize NativeMock at the start of the process. This should be done as early as possible as any P/Invoke method that is called before the initialization/registration cannot be mocked (see limitation above). The preferred method of initialization is done using a module initializer:

using System.Runtime.CompilerServices;

public static class ModuleInitializer
{
  [ModuleInitializer]
  public static void Initialize()
  {
    NativeMockRepository.Initialize();
    
    // auto-detect suitable interfaces
    NativeMockRepository.RegisterFromAssembly (typeof(ModuleInitializer).Assembly);
      
    // or register them manually
    // NativeMockRepository.Register<INativeLibraryApi>();
  }
}

Usage

Hint: take a look at the NativeMockRepository and NativeLibraryDummy classes

To mock any native function call create an interface for it an apply the NativeMockInterface attribute to it. The DLL name provided with the attribute must match the native library used in the P/Invoke method declaration. The method signature should match up with its native counterpart. This includes any marshalling attribute applied to the parameters or the return value. Otherwise bad things are going to happen - see declaring type examples below on a good way to minimize the risk of non-matching signatures.

[DllImport("test.dll")]
public static extern int MyExternalFunction ([MarshalAs (UnmanagedType.LPUTF8Str)] string s);

[NativeMockInterface ("test.dll")]
public interface INativeLibraryApi
{
  int MyExternalFunction ([MarshalAs (UnmanagedType.LPUTF8Str)] string s);
}

It is possible to override settings for a specific method by using the NativeMockCallback attribute. This allows you to use different names for the native function and interface method. This is especially useful if the native function contains a prefix/suffix which would unnecessarily pollute the method name.

[DllImport("test.dll")]
public static extern int MTL_InitializeUtf8 ([MarshalAs (UnmanagedType.LPUTF8Str)] string s);

[NativeMockInterface ("test.dll")]
public interface INativeLibraryApi
{
  [NativeMockCallback ("MTL_InitializeUtf8")]
  int MyExternalFunction ([MarshalAs (UnmanagedType.LPUTF8Str)] string s);
}

Instead of copying marshaling attributes onto the method declaration it is possible to let NativeMock detect them from the native function declaration. This can be done by specifying a DeclaringType on either the NativeMockInterface attribute or the NativeMockCallback attribute. Settings from the interface attribute are inherited to each member which can override them with their own settings if needed.

To provide a level of safety, the method signatures must match the signatures of their native counterparts. If any of the parameter or return types does not match up, the registration will fail. If possible always provide a declaring type since it makes sure that declared native functions exist and that the marshaling works correctly. Differing marshaling attributes can cause hard to diagnose problems.

public class MyExternalFunctions
{
  [DllImport("test.dll")]
  public static extern int MyExternalFunction ([MarshalAs (UnmanagedType.LPUTF8Str)] string s);
}

[NativeMockInterface ("test.dll", DeclaringType = typeof(MyExternalFunctions))]
public interface INativeLibraryApi
{
  [NativeMockCallback(DeclaringType = ...)] // we can override the inherited value
  int MyExternalFunction (string s);
}

If no native mock interface is registered it will behave according to its Behavior set using the either the NativeMockInterface attribute or the NativeMockCallback attribute.

If a P/Invoke function is called but no setup is found it will behave according to its set NativeMockBehaviour, which can be set using the NativeMockInterfaceAttribute or NativeMockCallbackAttribute. The following behaviors are available:

  • Strict (default): The native function call will throw a NativeFunctionNotMockedException.
  • Loose: The native function call will do nothing and return a default value if required.
  • Forward: The original P/Invoke function is called and its result returned.

That's all that is needed to define a native mock interface. Register it as described in the Installation section or auto-detect your interfaces. Use the Setup on the NativeMockRepository to setup an implementation for your native mock. Pro tip: Use a mocking library to mock your native mock interfaces. You can remove any registered native mock implementation by calling Reset or ResetAll.

Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Framework net462 is compatible.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.2.1 257 5/10/2022