NSubstitute
NSubstitute copied to clipboard
optional "Strict mode" support
Is your feature request related to a problem? Please describe. Unconfigured method return default value and code continues after call - which can have hard to undo consequences ... Strict mode would throw exception instead ...
Describe the solution you'd like Implement Strict extension method that can be optionally called on substitute:
public interface IFoo
{
object Bar();
object Baz();
}
var foo = Substitute.For<IFoo>().Strict(o => {
o.Bar().Returns(42);
});
foo.Baz(); // this will throw because only Bar was configured
Describe alternatives you've considered
Manually add method().Throws<Exception>(); on every single method that is on type.
This will work, but may break if method is added to type but tests are not updated...
also some interfaces can be quite large and manual mocking everything will be tedious ...
Additional context my quick & dirty implementation for public methods on interface
public interface IFoo
{
object Bar();
object Baz();
}
static class StrictExtension
{
// ResultForCallSpec is private - this just exposes it to my code ...
private class ResultForCallSpecWrapper
{
private readonly object _Instance; // ResultForCallSpec
private static readonly Type _Type = typeof(CallResults).Assembly.GetType("NSubstitute.Core.CallResults+ResultForCallSpec");
private static readonly MethodInfo _IsResultForMethod = _Type.GetMethod("IsResultFor");
public ResultForCallSpecWrapper(object instance) => _Instance = instance;
public bool IsResultFor(ICall call) => (bool)_IsResultForMethod.Invoke(_Instance, new object[] { call });
}
public static IFoo Strict(this IFoo obj, Action<IFoo> action) {
// let user configure obj ...
action?.Invoke(obj);
// then add configuration to all unconfigured methods so they throw ...
// reflection used so i can access nsubstitute inners - if implemented in assembly it would probably not be needed ...
var type = obj.GetType();
var callRouterProvider = (ICallRouterProvider)type
.GetField("__mixin_NSubstitute_Core_ICallRouterProvider", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(obj);
var callRouter = callRouterProvider.GetCallRouter();
var substituteState = (ISubstituteState)callRouter
.GetType()
.GetField("<substituteState>P", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(callRouter);
var callResults = substituteState.CallResults;
var results = /*(ConcurrentStack<CallResults.ResultForCallSpec>)*/((ICollection)typeof(CallResults)
.GetField("_results", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(callResults))
.Cast<object>().Select(o => new ResultForCallSpecWrapper(o)).ToArray();
Func<ICall, bool> hasResultFor = call => results.Any(o => o.IsResultFor(call));
IReturn result = new ReturnValueFromFunc<object>(_ => throw new NotImplementedException("Not mocked"));
var methods = typeof(IFoo).GetMethods(BindingFlags.Instance | BindingFlags.Public);
foreach (var method in methods) {
var parameters = method.GetParameters();
var argumentSpecifications = parameters
.Select(o => new ArgumentSpecification(typeof(IFoo), new AnyArgumentMatcher(o.ParameterType)))
.ToArray();
var arguments = parameters
.Select(o => o.ParameterType.IsValueType ? Activator.CreateInstance(o.ParameterType) : null)
.ToArray();
var call = new Call(method, arguments, obj, argumentSpecifications, null);
if (!hasResultFor(call)) {
ICallSpecification callSpecification = new CallSpecification(method, argumentSpecifications);
substituteState.CallResults.SetResult(callSpecification, result);
}
}
// todo: non-methods
return obj;
}
}
public class Test
{
public void TestMe() {
var foo = Substitute.For<IFoo>().Strict(o => {
o.Bar().Returns(42);
});
var bar = foo.Bar(); // bar == 42
foo.Baz(); // this is throwing NotImplementedException("Not mocked")
}
}