Created
April 20, 2018 17:34
-
-
Save cjrpriest/70731bfd6b5f1d3803fcdff9074f4715 to your computer and use it in GitHub Desktop.
how can I create mocks around generic types, where I only know the types at runtime, and I also get full async/await functionality? This seems to be very hard, and maybe impossible...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Reflection; | |
using System.Threading.Tasks; | |
using Microsoft.CodeAnalysis; | |
using Microsoft.CodeAnalysis.CSharp; | |
using NSubstitute; | |
using NSubstitute.Core; | |
using Shouldly; | |
namespace ConsoleApp | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
new Program().Run().Wait(); | |
} | |
async Task Run() | |
{ | |
// Option 1 | |
// var doSomethingMock = new SetupMockWithGeneric() | |
// .SetItUp<ThingResult>(Substitute.For<IDoSomething>()); | |
// Option 2 | |
// var doSomethingMock = new SetupMockWithoutGenericUsingExpressions() | |
// .SetItUp(Substitute.For<IDoSomething>(), typeof(ThingResult)); | |
// Option 3 | |
var doSomethingMock = new SetupMockWithoutGenericUsingRoslyn() | |
.SetItUp(Substitute.For<IDoSomething>(), typeof(ThingResult)); | |
// Option 1 -- this assertion works | |
// Option 2 -- this assertion doens't work! Exception is unhandled, it bubbles up | |
// Option 3 -- if I could around the external assembly issue, I think this assertion would work | |
// Assertion | |
await doSomethingMock.DoSomething(new Thing()) | |
.ShouldThrowAsync<ItsBrokenException>(); | |
Console.WriteLine("All good"); | |
} | |
} | |
public interface IThing<TOutput> {} | |
public sealed class ThingResultWrapper<TResult>{} | |
// if i make this public, then the rosyln approach looks like it'll work... but that would mean the consumer of | |
// the library needs to expose everything as public | |
class Thing : IThing<ThingResult> {} | |
class ThingResult {} | |
// Option 1 | |
// This is the easy way to setup my mock, but it means i have to set types at compile time. Boo hiss. | |
class SetupMockWithGeneric | |
{ | |
public IDoSomething SetItUp<TResult>(IDoSomething doSomethingMock) | |
{ | |
doSomethingMock | |
.DoSomething(Arg.Any<IThing<TResult>>()) | |
.Returns(async ci => await BrokenClass.BrokenMethod<TResult>()); | |
return doSomethingMock; | |
} | |
} | |
// Option 2 | |
// This is a crap load of code, but it means types can be set at runtime! yey! | |
// The problem is that async/await is not supported in Expressions... which means that | |
// a) those optimisations are not realised | |
// b) Extensions methods like NSubstitutes ShouldThrowAsync<> do not work | |
class SetupMockWithoutGenericUsingExpressions | |
{ | |
public IDoSomething SetItUp(IDoSomething doSomethingMock, Type type) | |
{ | |
var doSomethingParameter = Expression.Parameter(typeof(IDoSomething)); | |
//.DoSomething(Arg.Any<T>()) | |
var thingOfTypeType = typeof(IThing<>).MakeGenericType(type); | |
var argAnyTExpression = Expression.Call(typeof(Arg), nameof(Arg.Any), new[] {thingOfTypeType}); | |
var doSomethingMethod = typeof(IDoSomething).GetMethods().First().MakeGenericMethod(type); | |
var doSomethingExpression = Expression.Call(doSomethingParameter, doSomethingMethod, | |
argAnyTExpression); | |
//.Returns(ci => BrokenClass.BrokenMethod(ci.Arg<T>()).Result); | |
var callInfoParameter = Expression.Parameter(typeof(CallInfo)); | |
var commandArgumentFromCallInfoExpression = Expression.Call(callInfoParameter, nameof(Arg), new[] {type}); | |
var brokenMethodMethod = typeof(BrokenClass).GetMethods().First().MakeGenericMethod(type); | |
var brokenMethodExpression = Expression.Call(brokenMethodMethod);//, commandArgumentFromCallInfoExpression); | |
var brokenMethodExpressionResult = Expression.Property(brokenMethodExpression, nameof(Task<object>.Result)); | |
var returnThisLambda = Expression.Lambda(brokenMethodExpressionResult, callInfoParameter); | |
var thingResultWrapperOfTypeType = typeof(ThingResultWrapper<>).MakeGenericType(type); | |
var returnsMethodGeneric = | |
typeof(SubstituteExtensions) | |
.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) | |
.First(x => x.Name == nameof(SubstituteExtensions.Returns) | |
&& IsParameterType(x.GetParameters()[0], typeof(Task<>)) | |
&& IsParameterType(x.GetParameters()[1], typeof(Func<,>))) | |
.MakeGenericMethod(thingResultWrapperOfTypeType); | |
var returnThisLambdaType = | |
typeof(Func<,>) | |
.MakeGenericType(typeof(CallInfo), thingResultWrapperOfTypeType); | |
var returnsExpression = | |
Expression.Call( | |
null, | |
returnsMethodGeneric, | |
doSomethingExpression, | |
returnThisLambda, | |
Expression.NewArrayInit(returnThisLambdaType)); | |
// compile it | |
var lambdaExpression = Expression.Lambda<Action<IDoSomething>>(returnsExpression, doSomethingParameter); | |
var lambda = lambdaExpression.Compile(); | |
lambda(doSomethingMock); | |
return doSomethingMock; | |
} | |
private static bool IsParameterType (ParameterInfo parameter, Type type) | |
{ | |
if (parameter.ParameterType.IsGenericType) | |
{ | |
if (!type.IsGenericType) return false; | |
return parameter.ParameterType.GetGenericTypeDefinition() == type; | |
} | |
return parameter.ParameterType == type; | |
} | |
} | |
// Option 3 | |
// This could be ideal -- don't need to know types at startup (hurrah!) and because we're using the full compiler | |
// we get async/await goodness that should work with ShouldThrowAsync<> (hurrah!) | |
// However, as roslyn compiles to another assembly, it is not able to see ThingResult (which is internal) | |
class SetupMockWithoutGenericUsingRoslyn | |
{ | |
public IDoSomething SetItUp(IDoSomething doSomethingMock, Type type) | |
{ | |
var code = SetItUpClassCode.Replace("<TResult>", $"<{type.Name}>"); | |
Console.WriteLine(code); | |
var syntaxTree = CSharpSyntaxTree.ParseText(code); | |
var compilation = CSharpCompilation.Create( | |
"CrispysDynamicAssembly", | |
new [] { syntaxTree }, | |
new [] { | |
MetadataReference.CreateFromFile(typeof(IThing<>).GetTypeInfo().Assembly.Location), | |
MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), | |
MetadataReference.CreateFromFile(typeof(Substitute).Assembly.Location), | |
MetadataReference.CreateFromFile(Assembly.GetExecutingAssembly().Location), | |
MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "System.Runtime.dll")), | |
MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "System.Threading.Tasks.dll")), | |
MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "System.Threading.Tasks.Extensions.dll")), | |
}, | |
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) | |
); | |
using (var ms = new MemoryStream()) | |
{ | |
var result = compilation.Emit(ms); | |
if (result.Success) | |
{ | |
Console.WriteLine("yey it compiled and will probably work"); | |
// i'll bother implementing this bit later | |
} | |
else | |
{ | |
foreach (var resultDiagnostic in result.Diagnostics) | |
{ | |
Console.WriteLine(resultDiagnostic); | |
} | |
} | |
} | |
return doSomethingMock; | |
} | |
private string SetItUpClassCode = @" | |
using NSubstitute; | |
using ConsoleApp; | |
class SetupMockWithGeneric | |
{ | |
public IDoSomething SetItUp<IThing>(IDoSomething doSomethingMock) | |
{ | |
doSomethingMock | |
.DoSomething(Arg.Any<IThing<TResult>>()) | |
.Returns(async ci => await BrokenClass.BrokenMethod<TResult>()); | |
return doSomethingMock; | |
} | |
}"; | |
} | |
public class BrokenClass | |
{ | |
public static Task<ThingResultWrapper<T>> BrokenMethod<T>() | |
{ | |
throw new ItsBrokenException(); | |
} | |
} | |
public interface IDoSomething | |
{ | |
Task<ThingResultWrapper<TResult>> DoSomething<TResult>(IThing<TResult> input); | |
} | |
public class ItsBrokenException: Exception {} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment