No ano passado, uma atualização .Net trouxe um recurso: geradores de código-fonte. Eu me perguntei o que era e decidi escrever um gerador de mock para que pegasse uma interface ou uma classe abstrata como entrada e produzisse mocks que pudessem ser usados em testes com compiladores aot. Quase imediatamente surgiu a questão: como testar o próprio gerador? Naquela época, o livro de receitas oficial não continha uma receita de como fazer direito. Posteriormente, esse problema foi corrigido, mas você pode estar interessado em ver como os testes funcionam no meu projeto.
O livro de receitas tem uma receita simples de exatamente como iniciar o gerador. Você pode jogá-lo contra um trecho de código-fonte e garantir que a geração seja concluída sem erros. E então surge a pergunta: como ter certeza de que o código foi criado e funciona corretamente? É claro que você pode pegar algum código de referência, analisá-lo usando CSharpSyntaxTree.ParseText e compará-lo usando IsEquivalentTo . No entanto, o código tende a mudar, e a comparação com o código funcionalmente idêntico, mas diferindo em comentários e caracteres de espaço em branco, me deu um resultado negativo. Vamos pelo caminho mais longo:
Vamos criar uma compilação;
Vamos criar e rodar um gerador;
Vamos construir a biblioteca e carregá-la no processo atual;
Vamos encontrar o código resultante e executá-lo.
Compilação
O compilador é iniciado usando a função CSharpCompilation.Create . Aqui você pode adicionar código e incluir links para bibliotecas. O código-fonte é preparado usando CSharpSyntaxTree.ParseText e as bibliotecas MetadataReference.CreateFromFile (há opções para fluxos e matrizes). Como obter o caminho? Na maioria dos casos, tudo é simples:
typeof(UnresolvedType).Assembly.Location
No entanto, em alguns casos, o tipo está no assembly de referência, então isso funciona:
Assembly.Load(new AssemblyName("System.Linq.Expressions")).Location
Assembly.Load(new AssemblyName("System.Runtime")).Location
Assembly.Load(new AssemblyName("netstandard")).Location
Como pode ser a criação de uma compilação
protected static CSharpCompilation CreateCompilation(string source, string compilationName)
=> CSharpCompilation.Create(compilationName,
syntaxTrees: new[]
{
CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Preview))
},
references: new[]
{
MetadataReference.CreateFromFile(Assembly.GetCallingAssembly().Location),
MetadataReference.CreateFromFile(typeof(string).Assembly.Location),
MetadataReference.CreateFromFile(typeof(LightMock.InvocationInfo).Assembly.Location),
MetadataReference.CreateFromFile(typeof(IMock<>).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Xunit.Assert).Assembly.Location),
MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("System.Linq.Expressions")).Location),
MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("System.Runtime")).Location),
MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("netstandard")).Location),
},
options: new CSharpCompilationOptions(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary));
Iniciando o gerador e criando a montagem
: CSharpGeneratorDriver.Create, , (aka AdditionalFiles csproj). CSharpGeneratorDriver.RunGeneratorsAndUpdateCompilation , . , ITestOutputHelper Xunit . , Output .
protected (ImmutableArray<Diagnostic> diagnostics, bool success, byte[] assembly) DoCompile(string source, string compilationName)
{
var compilation = CreateCompilation(source, compilationName);
var driver = CSharpGeneratorDriver.Create(
ImmutableArray.Create(new LightMockGenerator()),
Enumerable.Empty<AdditionalText>(),
(CSharpParseOptions)compilation.SyntaxTrees.First().Options);
driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, out var diagnostics);
var ms = new MemoryStream();
var result = updatedCompilation.Emit(ms);
foreach (var i in result.Diagnostics)
testOutputHelper.WriteLine(i.ToString());
return (diagnostics, result.Success, ms.ToArray());
}
.Net Core AssemblyLoadContext. . Assembly, . : . . dynamic - . , , . , , .
using System;
using Xunit;
namespace LightMock.Generator.Tests.Mock
{
public class AbstractClassWithBasicMethods : ITestScript<AAbstractClassWithBasicMethods>
{
// Mock<T>
private readonly Mock<AAbstractClassWithBasicMethods> mock;
public AbstractClassWithBasicMethods()
=> mock = new Mock<AAbstractClassWithBasicMethods>();
public IMock<AAbstractClassWithBasicMethods> Context => mock;
public AAbstractClassWithBasicMethods MockObject => mock.Object;
public int DoRun()
{
// Protected()
mock.Protected().Arrange(f => f.ProtectedGetSomething()).Returns(1234);
Assert.Equal(expected: 1234, mock.Object.InvokeProtectedGetSomething());
mock.Object.InvokeProtectedDoSomething(5678);
mock.Protected().Assert(f => f.ProtectedDoSomething(5678));
return 42;
}
}
}
, , : AnalyzerConfigOptionsProvider AnalyzerConfigOptions.
sealed class MockAnalyzerConfigOptions : AnalyzerConfigOptions
{
public static MockAnalyzerConfigOptions Empty { get; }
= new MockAnalyzerConfigOptions(ImmutableDictionary<string, string>.Empty);
private readonly ImmutableDictionary<string, string> backing;
public MockAnalyzerConfigOptions(ImmutableDictionary<string, string> backing)
=> this.backing = backing;
public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
=> backing.TryGetValue(key, out value);
}
sealed class MockAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider
{
private readonly ImmutableDictionary<object, AnalyzerConfigOptions> otherOptions;
public MockAnalyzerConfigOptionsProvider(AnalyzerConfigOptions globalOptions)
: this(globalOptions, ImmutableDictionary<object, AnalyzerConfigOptions>.Empty)
{ }
public MockAnalyzerConfigOptionsProvider(AnalyzerConfigOptions globalOptions,
ImmutableDictionary<object, AnalyzerConfigOptions> otherOptions)
{
GlobalOptions = globalOptions;
this.otherOptions = otherOptions;
}
public static MockAnalyzerConfigOptionsProvider Empty { get; }
= new MockAnalyzerConfigOptionsProvider(
MockAnalyzerConfigOptions.Empty,
ImmutableDictionary<object, AnalyzerConfigOptions>.Empty);
public override AnalyzerConfigOptions GlobalOptions { get; }
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree)
=> GetOptionsPrivate(tree);
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
=> GetOptionsPrivate(textFile);
AnalyzerConfigOptions GetOptionsPrivate(object o)
=> otherOptions.TryGetValue(o, out var options) ? options : MockAnalyzerConfigOptions.Empty;
}
CSharpGeneratorDriver.Create optionsProvider, . , . , .
- . , , . . .
, . .
, . , , , , ITestOutputHelper Xunit.
, CancellationToken. .
O gerador de simulação está aqui . Esta é uma versão beta e não é recomendada para uso em produção.