-
Notifications
You must be signed in to change notification settings - Fork 265
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ability to mock protected methods with and without return value #845
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me! WDYT @Romfos ?
public static object Protected<T>(this T obj, string methodName, params object[] args) where T : class | ||
{ | ||
if (obj == null) { throw new ArgumentNullException(nameof(obj), "Cannot mock null object"); } | ||
if (string.IsNullOrWhiteSpace(methodName)) { throw new ArgumentException("Must provide valid protected method name to mock", nameof(methodName)); } | ||
|
||
IList<IArgumentSpecification> argTypes = SubstitutionContext.Current.ThreadContext.PeekAllArgumentSpecifications(); | ||
MethodInfo mthdInfo = obj.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance, Type.DefaultBinder, argTypes.Select(x => x.ForType).ToArray(), null); | ||
|
||
if (mthdInfo == null) { throw new Exception($"Method {methodName} not found"); } | ||
if (!mthdInfo.IsVirtual) { throw new Exception($"Method {methodName} is not virtual"); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (nitpick): ideally should use a custom exception(subclassing SubstituteException
) with details on what they should do to fix the issue. (see NSubstitute.Exceptions
for examples.)
For obj == null
, we have NullSubstituteReferenceException
. Should also ensure receiver is a substitute otherwise throw NotASubstituteException
.
For mthdInfo == null
, maybe include arg types checked, something like "No method found with signature Foo(Int, String) on IMySubstitute. Check the method name and arguments are correct."
For non-virtual, can probably use a summary of the warning given in the documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, this makes sense
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made the suggested changes and added additional unit tests to check the error conditions...which helped me find a problem
If you run these tests (in sequence) the second and third test will fail. Removing the first test, the other tests run successfully:
- Should_throw_on_mock_method_arg_mismatch
- Should_throw_on_mock_non_virtual
- Should_throw_on_mock_non_virtual_void_method
I tracked it down to orphaned arg spec from test 1 because ThreadLocalContext.DequeueAllArgumentSpecifications
is not invoked when the method is invalid (even if I did not throw exception when method is not virtual). I am now dequeuing all arg specs before I throw exception. Not sure if this is the best approach?
public IList<IArgumentSpecification> PeekAllArgumentSpecifications() | ||
{ | ||
var queue = _argumentSpecifications.Value; | ||
if (queue == null) { throw new SubstituteInternalException("Argument specification queue is null."); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: do you know under what circumstances this occurs? If it is expected can we just return EmptySpecifications
in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tbh it doesn't look like it could ever be null. I decided to have/keep this in line with enqueue and dequeue methods in case it is something I failed to see
Should we change all 3 (along with enqueue, dequeue) methods to provide a consistent behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only changed PeekAllArgumentSpecifications
, but happy to update DequeueAllArgumentSpecifications if you think it makes sense
… unit tests to verify error conditions * Rename unit tests with better description * Fix unit test failure due to orphaned argument spec when previous setup throws
Closes #800