Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/docs/writing-tests/mocking/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,47 @@ var value = serviceB.GetValue(); // 42

Use `Mock.Get(obj)` to retrieve the `Mock<T>` wrapper for any mock object — auto-mocked return values, or any object created by `T.Mock()`. Auto-mocks are cached — calling the same method returns the same mock instance.

### Runtime Auto-Stubs

Source-generated auto-mocks cover every interface the generator can name at compile time. Some
types it structurally cannot see — most commonly a generic method like `T Get<T>()` invoked
*inside* a third-party SDK with a `T` that is `internal` to that SDK, so your test assembly
cannot even write the type name:

```csharp
// Inside the Azure Functions Worker SDK — not your code:
// features.Get<IFunctionBindingsFeature>() // IFunctionBindingsFeature is internal to the SDK

var features = IInvocationFeatures.Mock();

// The SDK's internal Get<T>() call receives a functional runtime stub instead of null.
```

In loose mode, when no source-generated mock exists for a requested interface, TUnit.Mocks emits
a functional stub at runtime. Stubs are recursive and use the same defaults you'd expect from
runtime-proxy libraries like NSubstitute: strings return `""`, tasks come back completed,
collections are empty, value types are zeroed, and interface-returning members return further
stubs (or a real configurable `Mock<T>` when a source-generated factory exists for that type).
Properties round-trip values set on them, and member results are cached for stable identity.

The stub assembly is named `DynamicProxyGenAssembly2` and carries Castle DynamicProxy's public
key — the exact identity SDKs already grant `InternalsVisibleTo` for NSubstitute/Moq
compatibility — so any internal interface reachable by those libraries is reachable by
TUnit.Mocks stubs too.

Notes and limits:

- Runtime stubs are not configurable or verifiable — by definition you cannot name their types
in test code. Nameable interfaces still get real, configurable mocks.
- Strict mode is unaffected: unconfigured calls still throw.
- On Native AOT (where `Reflection.Emit` does not exist) the feature is inert and unconfigured
calls keep returning default values.
- The `netstandard2.0` asset (used by .NET Framework test projects) does not include the
runtime emitter — unconfigured calls return default values there too. Runtime stubs require
the `net8.0`+ assets.
- Opt out globally with `settings.Mocks.RuntimeAutoStubs = false;` in a
`[Before(HookType.TestDiscovery)]` hook.

## MockRepository

Manage multiple mocks with shared behavior and batch operations:
Expand Down
80 changes: 16 additions & 64 deletions src/TUnit.Mocks/MockEngine.Typed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,9 @@ public TReturn HandleCallWithReturn<TReturn, T1>(int memberId, string memberName
}
#pragma warning restore IL3050, IL2026

if (Behavior == MockBehavior.Loose && typeof(TReturn).IsInterface)
if (TryGetLooseAutoMockResult(memberName, autoMockFactory: null, out TReturn autoMockResult))
{
var cacheKey = memberName + "|" + typeof(TReturn).FullName;
var autoMock = AutoMockCache.GetOrAdd(cacheKey, _ =>
{
MockRegistry.TryCreateAutoMock(typeof(TReturn), Behavior, out var m);
return m;
});
if (autoMock is not null) return (TReturn)autoMock.ObjectInstance;
return autoMockResult;
}

if (Behavior == MockBehavior.Strict)
Expand Down Expand Up @@ -277,15 +271,9 @@ public TReturn HandleCallWithReturn<TReturn, T1, T2>(int memberId, string member
}
#pragma warning restore IL3050, IL2026

if (Behavior == MockBehavior.Loose && typeof(TReturn).IsInterface)
if (TryGetLooseAutoMockResult(memberName, autoMockFactory: null, out TReturn autoMockResult))
{
var cacheKey = memberName + "|" + typeof(TReturn).FullName;
var autoMock = AutoMockCache.GetOrAdd(cacheKey, _ =>
{
MockRegistry.TryCreateAutoMock(typeof(TReturn), Behavior, out var m);
return m;
});
if (autoMock is not null) return (TReturn)autoMock.ObjectInstance;
return autoMockResult;
}

if (Behavior == MockBehavior.Strict)
Expand Down Expand Up @@ -435,15 +423,9 @@ public TReturn HandleCallWithReturn<TReturn, T1, T2, T3>(int memberId, string me
}
#pragma warning restore IL3050, IL2026

if (Behavior == MockBehavior.Loose && typeof(TReturn).IsInterface)
if (TryGetLooseAutoMockResult(memberName, autoMockFactory: null, out TReturn autoMockResult))
{
var cacheKey = memberName + "|" + typeof(TReturn).FullName;
var autoMock = AutoMockCache.GetOrAdd(cacheKey, _ =>
{
MockRegistry.TryCreateAutoMock(typeof(TReturn), Behavior, out var m);
return m;
});
if (autoMock is not null) return (TReturn)autoMock.ObjectInstance;
return autoMockResult;
}

if (Behavior == MockBehavior.Strict)
Expand Down Expand Up @@ -593,15 +575,9 @@ public TReturn HandleCallWithReturn<TReturn, T1, T2, T3, T4>(int memberId, strin
}
#pragma warning restore IL3050, IL2026

if (Behavior == MockBehavior.Loose && typeof(TReturn).IsInterface)
if (TryGetLooseAutoMockResult(memberName, autoMockFactory: null, out TReturn autoMockResult))
{
var cacheKey = memberName + "|" + typeof(TReturn).FullName;
var autoMock = AutoMockCache.GetOrAdd(cacheKey, _ =>
{
MockRegistry.TryCreateAutoMock(typeof(TReturn), Behavior, out var m);
return m;
});
if (autoMock is not null) return (TReturn)autoMock.ObjectInstance;
return autoMockResult;
}

if (Behavior == MockBehavior.Strict)
Expand Down Expand Up @@ -751,15 +727,9 @@ public TReturn HandleCallWithReturn<TReturn, T1, T2, T3, T4, T5>(int memberId, s
}
#pragma warning restore IL3050, IL2026

if (Behavior == MockBehavior.Loose && typeof(TReturn).IsInterface)
if (TryGetLooseAutoMockResult(memberName, autoMockFactory: null, out TReturn autoMockResult))
{
var cacheKey = memberName + "|" + typeof(TReturn).FullName;
var autoMock = AutoMockCache.GetOrAdd(cacheKey, _ =>
{
MockRegistry.TryCreateAutoMock(typeof(TReturn), Behavior, out var m);
return m;
});
if (autoMock is not null) return (TReturn)autoMock.ObjectInstance;
return autoMockResult;
}

if (Behavior == MockBehavior.Strict)
Expand Down Expand Up @@ -909,15 +879,9 @@ public TReturn HandleCallWithReturn<TReturn, T1, T2, T3, T4, T5, T6>(int memberI
}
#pragma warning restore IL3050, IL2026

if (Behavior == MockBehavior.Loose && typeof(TReturn).IsInterface)
if (TryGetLooseAutoMockResult(memberName, autoMockFactory: null, out TReturn autoMockResult))
{
var cacheKey = memberName + "|" + typeof(TReturn).FullName;
var autoMock = AutoMockCache.GetOrAdd(cacheKey, _ =>
{
MockRegistry.TryCreateAutoMock(typeof(TReturn), Behavior, out var m);
return m;
});
if (autoMock is not null) return (TReturn)autoMock.ObjectInstance;
return autoMockResult;
}

if (Behavior == MockBehavior.Strict)
Expand Down Expand Up @@ -1067,15 +1031,9 @@ public TReturn HandleCallWithReturn<TReturn, T1, T2, T3, T4, T5, T6, T7>(int mem
}
#pragma warning restore IL3050, IL2026

if (Behavior == MockBehavior.Loose && typeof(TReturn).IsInterface)
if (TryGetLooseAutoMockResult(memberName, autoMockFactory: null, out TReturn autoMockResult))
{
var cacheKey = memberName + "|" + typeof(TReturn).FullName;
var autoMock = AutoMockCache.GetOrAdd(cacheKey, _ =>
{
MockRegistry.TryCreateAutoMock(typeof(TReturn), Behavior, out var m);
return m;
});
if (autoMock is not null) return (TReturn)autoMock.ObjectInstance;
return autoMockResult;
}

if (Behavior == MockBehavior.Strict)
Expand Down Expand Up @@ -1225,15 +1183,9 @@ public TReturn HandleCallWithReturn<TReturn, T1, T2, T3, T4, T5, T6, T7, T8>(int
}
#pragma warning restore IL3050, IL2026

if (Behavior == MockBehavior.Loose && typeof(TReturn).IsInterface)
if (TryGetLooseAutoMockResult(memberName, autoMockFactory: null, out TReturn autoMockResult))
{
var cacheKey = memberName + "|" + typeof(TReturn).FullName;
var autoMock = AutoMockCache.GetOrAdd(cacheKey, _ =>
{
MockRegistry.TryCreateAutoMock(typeof(TReturn), Behavior, out var m);
return m;
});
if (autoMock is not null) return (TReturn)autoMock.ObjectInstance;
return autoMockResult;
}

if (Behavior == MockBehavior.Strict)
Expand Down
57 changes: 50 additions & 7 deletions src/TUnit.Mocks/MockEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ private sealed class CallArrays(CallRecordBuffer?[] buffers, int[] counts)
private ConcurrentQueue<(string EventName, bool IsSubscribe)>? _eventSubscriptions;
private ConcurrentDictionary<string, Action>? _onSubscribeCallbacks;
private ConcurrentDictionary<string, Action>? _onUnsubscribeCallbacks;
private ConcurrentDictionary<string, IMock?>? _autoMockCache;
// Keyed by member AND the actual Type identity (never a name string): two same-full-named
// types from different assemblies must not share a cached auto-mock/stub.
private ConcurrentDictionary<(string MemberName, Type ReturnType), IMock?>? _autoMockCache;

/// <summary>
/// The current state name for state machine mocking. Null means no state (all setups match).
Expand Down Expand Up @@ -123,24 +125,32 @@ private ConcurrentDictionary<string, Action> OnSubscribeCallbacks
private ConcurrentDictionary<string, Action> OnUnsubscribeCallbacks
=> LazyInitializer.EnsureInitialized(ref _onUnsubscribeCallbacks)!;

private ConcurrentDictionary<string, IMock?> AutoMockCache
private ConcurrentDictionary<(string MemberName, Type ReturnType), IMock?> AutoMockCache
=> LazyInitializer.EnsureInitialized(ref _autoMockCache)!;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetLooseAutoMockResult<TReturn>(string memberName, Func<MockBehavior, IMock>? autoMockFactory, out TReturn result)
{
if (Behavior == MockBehavior.Loose && typeof(TReturn).IsInterface)
{
var cacheKey = memberName + "|" + typeof(TReturn).FullName;
var cacheKey = (memberName, typeof(TReturn));
var autoMock = AutoMockCache.GetOrAdd(cacheKey, _ =>
{
if (autoMockFactory is not null)
{
return autoMockFactory(Behavior);
}

MockRegistry.TryCreateAutoMock(typeof(TReturn), Behavior, out var mock);
return mock;
if (MockRegistry.TryCreateAutoMock(typeof(TReturn), Behavior, out var mock))
{
return mock;
}

// No source-generated factory — the type is either internal to another assembly
// (unnameable at compile time, #6514) or was simply never mocked. Fall back to a
// runtime-emitted stub where the platform allows it.
RuntimeStubs.RuntimeStubGenerator.TryCreateStub(typeof(TReturn), out var stub);
return stub;
Comment thread
thomhurst marked this conversation as resolved.
});

if (autoMock is not null)
Expand Down Expand Up @@ -656,7 +666,28 @@ public Diagnostics.MockDiagnostics GetDiagnostics()
}

/// <summary>
/// Tries to get a cached auto-mock by its cache key. Used by Mock&lt;T&gt;.GetAutoMock.
/// Tries to get a cached auto-mock for a member and return type.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool TryGetAutoMock(string memberName, Type returnType, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out IMock? mock)
Comment thread
thomhurst marked this conversation as resolved.
{
if (Volatile.Read(ref _autoMockCache) is not { } cache)
{
mock = null;
return false;
}

// A cached null records a definitive miss (stubs disabled/unavailable/failed) — not a
// usable mock, and NotNullWhen(true) promises non-null on true.
return cache.TryGetValue((memberName, returnType), out mock) && mock is not null;
}

/// <summary>
/// Tries to get a cached auto-mock by the legacy string cache key
/// (<c>memberName + "|" + returnType.FullName</c>). Kept for binary compatibility with
/// assemblies compiled against the previous shape of this helper; new code uses the
/// <see cref="TryGetAutoMock(string, Type, out IMock?)"/> overload, which keys by Type
/// identity instead of a name string.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool TryGetAutoMock(string cacheKey, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out IMock? mock)
Expand All @@ -666,7 +697,19 @@ public bool TryGetAutoMock(string cacheKey, [System.Diagnostics.CodeAnalysis.Not
mock = null;
return false;
}
return cache.TryGetValue(cacheKey, out mock);

foreach (var entry in cache)
{
if (entry.Value is not null &&
string.Equals(entry.Key.MemberName + "|" + entry.Key.ReturnType.FullName, cacheKey, StringComparison.Ordinal))
{
mock = entry.Value;
return true;
}
}

mock = null;
return false;
}

/// <summary>
Expand Down
Loading
Loading