NativeMock 0.2.1
dotnet add package NativeMock --version 0.2.1
NuGet\Install-Package NativeMock -Version 0.2.1
<PackageReference Include="NativeMock" Version="0.2.1" />
paket add NativeMock --version 0.2.1
#r "nuget: NativeMock, 0.2.1"
// 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
, andForward
. seeNativeMockBehavior
) - 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
- Requires setting a
- 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
)
- Most of the time, the setup is done using an
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
andNativeLibraryDummy
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 | Versions 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. |
-
.NETFramework 4.6.2
- System.Collections.Immutable (>= 5.0.0)
-
net5.0
- No dependencies.
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 |