昨年、.Netアップデートにより、ソースコードジェネレーターという機能が導入されました。それが何であるか疑問に思い、インターフェイスまたは抽象クラスを入力として受け取り、aotコンパイラでのテストに使用できるモックを生成するようにモックジェネレータを作成することにしました。すぐに疑問が生じました:ジェネレータ自体をテストする方法は?当時、公式クックブックにはそれを正しく行うためのレシピが含まれていませんでした。後でこの問題は修正されましたが、私のプロジェクトでテストがどのように機能するかを知りたいと思うかもしれません。
クックブックには、ジェネレーターを起動する方法についての簡単なレシピがあります。ソースコードと対戦して、エラーなしで生成が完了することを確認できます。そして、疑問が生じます。コードが正しく作成され、正しく機能することを確認する方法は?もちろん、いくつかの参照コードを取得し、CSharpSyntaxTree.ParseTextを使用して解析してから、IsEquivalentToを使用して比較することができます。ただし、コードは変更される傾向があり、機能的には同じであるがコメントと空白文字が異なるコードとの比較では、否定的な結果が得られました。長い道のりを進みましょう:
コンパイルを作成しましょう。
ジェネレーターを作成して実行しましょう。
ライブラリをビルドして、現在のプロセスにロードしましょう。
そこで結果のコードを見つけて実行しましょう。
コンパイル
コンパイラは、CSharpCompilation.Create関数を使用して起動されます。ここで、コードを追加し、ライブラリへのリンクを含めることができます。ソースコードは、CSharpSyntaxTree.ParseTextとMetadataReference.CreateFromFileライブラリを使用して準備されます(ストリームと配列のオプションがあります)。パスを取得する方法は?ほとんどの場合、すべてが単純です。
typeof(UnresolvedType).Assembly.Location
ただし、タイプが参照アセンブリにある場合は、次のように機能します。
Assembly.Load(new AssemblyName("System.Linq.Expressions")).Location
Assembly.Load(new AssemblyName("System.Runtime")).Location
Assembly.Load(new AssemblyName("netstandard")).Location
コンパイルの作成はどのように見えるか
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));
ジェネレータを起動してアセンブリを作成する
: 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. .
モックジェネレーターはこちらです。これはベータ版であり、本番環境での使用はお勧めしません。