Skip to content

Consider Supporting Open Generics #287

Description

@JasonBock

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);
}

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions