Describe the solution you'd like
Rocks has always forced users to close generic types. For example, if you had this:
public interface IService<T, TReturn>
{
TReturn Service(T data);
}
You had to provide types to create the mock:
var expectations = Rock.Create<IService<int, string>>();
I know there are reasons why I forced this and didn't support open generics, but....with the new approach of using attributes on the way, I think it's worth revisiting. You'd still be able to pass in closed generics, but I think this is possible:
[RockCreate(typeof(IService<,>))]
I'll add a code example in a follow-up comment. But, I think this is possible. And the generated expectations name is cleaner to me: IServiceCreateExpectations<T, TReturn>. Furthermore, you wouldn't have to generate mock code for each closed generic; you could specify you want this, and the types could be specified when you start creating expectations. Here's a code dump of a POC (I've already verified this would pass the test :) ):
// <auto-generated/>
#nullable enable
using NUnit.Framework;
public static class Playground
{
[Test]
public static void Go()
{
var intStringExpectations = new IServiceCreateExpectations<int, string>();
intStringExpectations.Methods.Service(3).ReturnValue("three");
var intStringMock = intStringExpectations.Instance();
Assert.AreEqual("three", intStringMock.Service(3));
intStringExpectations.Verify();
var stringIntExpectations = new IServiceCreateExpectations<string, int>();
stringIntExpectations.Methods.Service("four").ReturnValue(4);
var stringIntMock = stringIntExpectations.Instance();
Assert.AreEqual(4, stringIntMock.Service("four"));
stringIntExpectations.Verify();
}
}
internal sealed class IServiceCreateExpectations<T, TReturn>
: global::Rocks.Expectations.ExpectationsV4
{
#pragma warning disable CS8618
internal sealed class Handler0
: global::Rocks.HandlerV4<global::System.Func<T, TReturn>, TReturn>
{
public global::Rocks.Argument<T> @data { get; set; }
}
#pragma warning restore CS8618
private readonly global::System.Collections.Generic.List<global::IServiceCreateExpectations<T, TReturn>.Handler0> @handlers0 = new();
public override void Verify()
{
if (this.WasInstanceInvoked)
{
var failures = new global::System.Collections.Generic.List<string>();
failures.AddRange(this.Verify(handlers0));
if (failures.Count > 0)
{
throw new global::Rocks.Exceptions.VerificationException(failures);
}
}
}
private sealed class RockIService
: global::IService<T, TReturn>
{
public RockIService(global::IServiceCreateExpectations<T, TReturn> @expectations)
{
this.Expectations = @expectations;
}
[global::Rocks.MemberIdentifier(0, "TReturn Service(T @data)")]
public TReturn Service(T @data)
{
if (this.Expectations.handlers0.Count > 0)
{
foreach (var @handler in this.Expectations.handlers0)
{
if (@handler.@data.IsValid(@data!))
{
@handler.CallCount++;
var @result = @handler.Callback is not null ?
@handler.Callback(@data!) : @handler.ReturnValue;
return @result!;
}
}
throw new global::Rocks.Exceptions.ExpectationException("No handlers match for TReturn Service(T @data)");
}
throw new global::Rocks.Exceptions.ExpectationException("No handlers were found for TReturn Service(T @data)");
}
private global::IServiceCreateExpectations<T, TReturn> Expectations { get; }
}
internal sealed class IServiceMethodExpectations
{
internal IServiceMethodExpectations(global::IServiceCreateExpectations<T, TReturn> expectations) =>
this.Expectations = expectations;
internal global::Rocks.AdornmentsV4<global::IServiceCreateExpectations<T, TReturn>.Handler0, global::System.Func<T, TReturn>, TReturn> Service(global::Rocks.Argument<T> @data)
{
global::System.ArgumentNullException.ThrowIfNull(@data);
var handler = new global::IServiceCreateExpectations<T, TReturn>.Handler0
{
@data = @data,
};
this.Expectations.handlers0.Add(handler);
return new(handler);
}
private global::IServiceCreateExpectations<T, TReturn> Expectations { get; }
}
internal global::IServiceCreateExpectations<T, TReturn>.IServiceMethodExpectations Methods { get; }
internal IServiceCreateExpectations() =>
(this.Methods) = (new(this));
internal global::IService<T, TReturn> Instance()
{
if (!this.WasInstanceInvoked)
{
this.WasInstanceInvoked = true;
var @mock = new RockIService(this);
this.MockType = @mock.GetType();
return @mock;
}
else
{
throw new global::Rocks.Exceptions.NewMockInstanceException("Can only create a new mock once.");
}
}
}
public interface IService<T, TReturn>
{
TReturn Service(T data);
}
Describe the solution you'd like
Rocks has always forced users to close generic types. For example, if you had this:
You had to provide types to create the mock:
I know there are reasons why I forced this and didn't support open generics, but....with the new approach of using attributes on the way, I think it's worth revisiting. You'd still be able to pass in closed generics, but I think this is possible:
I'll add a code example in a follow-up comment. But, I think this is possible. And the generated expectations name is cleaner to me:
IServiceCreateExpectations<T, TReturn>. Furthermore, you wouldn't have to generate mock code for each closed generic; you could specify you want this, and the types could be specified when you start creating expectations. Here's a code dump of a POC (I've already verified this would pass the test :) ):