Skip to content

Instantly share code, notes, and snippets.

@cjrpriest
Created April 20, 2018 17:34
Show Gist options
  • Save cjrpriest/70731bfd6b5f1d3803fcdff9074f4715 to your computer and use it in GitHub Desktop.
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...
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