-
Notifications
You must be signed in to change notification settings - Fork 23
Description
First off, just want to say that this library is amazing! I've been migrating a few of my libraries from NSubstitute and I actually enjoy the process because it feels like a much cleaner syntax.
One bit of feedback I have is that the experience with mocking methods with out parameters does have a few friction points.
For example, one thing that was not immediately obvious is that a Callback() will always take precedence over ReturnValue() regardless of adornment order.
Take this method signature for example:
bool TryGet(string key, out int value);To mock this my first thought would be to write something like:
const string ExpectedKey = "expected";
const int ExpectedValue = 10;
const bool ExpectedReturn = true;
ILookupCreateExpectations expectations = new();
_ = expectations.Methods.TryGet(key: ExpectedKey, value: ExpectedValue)
.Callback(static (_, out valueOut) =>
{
valueOut = ExpectedValue;
return default;
})
.ReturnValue(ExpectedReturn);Digging into the generated code it makes sense why. Not a problem once you know.
var @result = @handler.Callback is not null ?
@handler.Callback(@key!, out @value!) : @handler.ReturnValue;Another noteworthy discovery is that the input validation for an out parameter always gets overridden with Arg.Any<T>(). The generated code looks something like:
internal global::Testing.ILookupCreateExpectations.Adornments.AdornmentsForHandler0 TryGet(global::Rocks.Argument<string> @key, global::Rocks.Argument<int> @value)
{
global::Rocks.Exceptions.ExpectationException.ThrowIf(this.Expectations.WasInstanceInvoked);
global::System.ArgumentNullException.ThrowIfNull(@key);
global::System.ArgumentNullException.ThrowIfNull(@value);
var @handler = new global::Testing.ILookupCreateExpectations.Handler0
{
@key = @key,
@value = global::Rocks.Arg.Any<int>(), // See here
};Lastly, I feel like there is just a bit too much ceremony in setting up a mock for a method with an out parameter so where applicable in my code I write some helper methods for a cleaner look like:
internal static ILookupCreateExpectations.Adornments.AdornmentsForHandler0 TryGet(this ILookupCreateExpectations expectations, Argument<string> @key, int @value, bool @returnValue) => expectations.Methods
.TryGet(@key, Arg.Any<int>())
.Callback((_, out valueOut) =>
{
valueOut = @value;
return @returnValue;
});
// Caller can then be like
expectations.TryGet(key: ExpectedKey, value: ExpectedValue, returnValue: ExpectedReturn);EDIT: I was thinking more about this and to support ref structs, a Func<T> approach is probably better:
+ internal static ILookupCreateExpectations.Adornments.AdornmentsForHandler0 TryGet(this ILookupCreateExpectations expectations, Argument<string> @key, Func<int> @value, Func<bool> @returnValue) => expectations.Methods
.TryGet(@key, Arg.Any<int>())
.Callback((_, out valueOut) =>
{
+ valueOut = @value();
+ return @returnValue();
});Would be good if something like this could be easier.
I originally thought that maybe generating an overload with the CallbackForHandler delegate might help but not sure how that should look if there are multiple out parameters.
Also a nit pick for RefStructArgument<T> but it would be nice if there were some way to avoid having to use new():
bool TryGet(ReadOnlySpan<char> key, out int value);
expectations.Methods.TryGet(key: new(x => x is "expected"), value: Arg.Any<int>());Something like a Predicate<T> generated overload or a helper similar to Arg.Validate() but for RefStructArgument<T> might be more consistent.
expectations.Methods.TryGet(key: x => x is "expected", value: Arg.Any<int>());
expectations.Methods.TryGet(key: RefArg.Validate(x => x is "expected"), value: Arg.Any<int>())I will be using this as my sole mocking library going forward! 🎉
I do have some previous experience developing source generators so I'm happy to contribute. I'll join the Discord.