Summary
When migrating from NSubstitute to TUnit.Mocks, I hit a case where a mocked type's generic method is called internally by a third-party SDK with a type argument that our assembly cannot name — an internal type in that SDK. NSubstitute can still handle this transparently; TUnit.Mocks structurally cannot, because it's a compile-time source generator running in the consuming assembly rather than a runtime proxy.
Context
Microsoft.Azure.Functions.Worker.FunctionContext.Features is an IInvocationFeatures, whose Get<T>() is called inside the Worker SDK itself (via context.GetInvocationResult()) with T = Microsoft.Azure.Functions.Worker.Context.Features.IFunctionBindingsFeature — an internal type. Our test assembly has no InternalsVisibleTo grant to that SDK assembly, so we cannot even write IFunctionBindingsFeature in our code, let alone configure a mock for it.
What NSubstitute does that works
using NSubstitute;
var features = Substitute.For<IInvocationFeatures>();
// We never configure Get<IFunctionBindingsFeature>() at all - we can't even name the type.
// But when the Worker SDK internally calls features.Get<IFunctionBindingsFeature>(),
// NSubstitute's Castle DynamicProxy transparently returns an auto-generated substitute
// for ANY T, regardless of its accessibility to our assembly - because Castle's proxy
// assembly (DynamicProxyGenAssembly2) is one of the assemblies the Worker SDK grants
// InternalsVisibleTo, while our own assembly is not.
context.Features.Returns(features);
This works because NSubstitute's substitution happens at runtime, inside an assembly the target SDK already trusts.
What TUnit.Mocks does instead
using TUnit.Mocks;
var features = IInvocationFeatures.Mock();
context.Features.Returns(features);
// Later, when Microsoft.Azure.Functions.Worker's own extension method internally calls:
// features.Get<IFunctionBindingsFeature>()
// TUnit.Mocks generated Get<T>() overloads only for T's our compile-time code actually
// asked to configure. For any other T - especially one we structurally cannot name because
// it's internal to another assembly - the call falls through to a plain unconfigured
// default (null), and there is no API to register "whatever T is asked for, return X"
// the way NSubstitute's proxy does implicitly.
The observable symptom is a NullReferenceException deep inside Worker SDK code that has nothing to do with the test's actual intent, because Get<IFunctionBindingsFeature>() silently returns null instead of an auto-substitute.
Minimal repro shape
public interface IFeatures
{
T Get<T>();
}
internal interface IInternalOnly { } // simulates a type from another assembly we can't name
public class Consumer
{
public string Run(IFeatures features)
{
// Simulates a third-party SDK internally requesting an internal type we can't reference
var internalFeature = features.Get<IInternalOnly>(); // we can't write this line ourselves
return internalFeature is null ? "null" : "configured";
}
}
With NSubstitute, Substitute.For<IFeatures>() returns "configured" for any T without any setup. With IFeatures.Mock(), there is no way to make an unconfigured/unnameable T resolve to anything but the default value.
Question / ask
Is this a known, accepted limitation of the compile-time-generator approach (i.e. "if you need this, keep NSubstitute for that one mock"), or is there a supported escape hatch we're missing — e.g. a way to register a fallback handler for Get<T>() regardless of T, or a lower-level hook that isn't restricted to types nameable by the source generator? If it's a hard limitation, it'd be worth calling out explicitly in the docs so people migrating from NSubstitute know to expect it for this shape of code (generic accessor methods called by third-party internals).
Versions
Summary
When migrating from NSubstitute to
TUnit.Mocks, I hit a case where a mocked type's generic method is called internally by a third-party SDK with a type argument that our assembly cannot name — aninternaltype in that SDK. NSubstitute can still handle this transparently;TUnit.Mocksstructurally cannot, because it's a compile-time source generator running in the consuming assembly rather than a runtime proxy.Context
Microsoft.Azure.Functions.Worker.FunctionContext.Featuresis anIInvocationFeatures, whoseGet<T>()is called inside the Worker SDK itself (viacontext.GetInvocationResult()) withT = Microsoft.Azure.Functions.Worker.Context.Features.IFunctionBindingsFeature— aninternaltype. Our test assembly has noInternalsVisibleTogrant to that SDK assembly, so we cannot even writeIFunctionBindingsFeaturein our code, let alone configure a mock for it.What NSubstitute does that works
This works because NSubstitute's substitution happens at runtime, inside an assembly the target SDK already trusts.
What TUnit.Mocks does instead
The observable symptom is a
NullReferenceExceptiondeep inside Worker SDK code that has nothing to do with the test's actual intent, becauseGet<IFunctionBindingsFeature>()silently returnsnullinstead of an auto-substitute.Minimal repro shape
With NSubstitute,
Substitute.For<IFeatures>()returns"configured"for anyTwithout any setup. WithIFeatures.Mock(), there is no way to make an unconfigured/unnameableTresolve to anything but the default value.Question / ask
Is this a known, accepted limitation of the compile-time-generator approach (i.e. "if you need this, keep NSubstitute for that one mock"), or is there a supported escape hatch we're missing — e.g. a way to register a fallback handler for
Get<T>()regardless ofT, or a lower-level hook that isn't restricted to types nameable by the source generator? If it's a hard limitation, it'd be worth calling out explicitly in the docs so people migrating from NSubstitute know to expect it for this shape of code (generic accessor methods called by third-party internals).Versions
TUnit.Mocks1.62.0dailydevops/http.correlationissue Feature: Injectable data via required properties & object initializer #723 (NSubstitute → TUnit.Mocks migration), same category of finding asdailydevops/healthchecksPR chore(deps): update tunit to 0.17.14 #2051's scoped exceptions ([TUnit.Mocks] Mock() fails for IDocumentStore (RavenDB.Client): asymmetric-accessor member CS0535/CS0548/CS0551 #6491–[TUnit.Mocks] No way to make an async member's .Returns() genuinely pending (only synchronous Func<T> supported) - breaks timeout-race tests #6495), but a structural gap rather than a per-type generator bug.