Skip to content

Add Expectations Overloads for Methods With out Parameters #366

@MitchRazga

Description

@MitchRazga

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions