Letztes Jahr brachte ein .Net-Update eine Funktion: Quellcode-Generatoren. Ich fragte mich, was es war, und entschied mich, einen Mock-Generator zu schreiben, der eine Schnittstelle oder eine abstrakte Klasse als Eingabe verwendete und Mocks erzeugte, die beim Testen mit aot-Compilern verwendet werden können. Fast sofort stellte sich die Frage: Wie teste ich den Generator selbst? Zu dieser Zeit enthielt das offizielle Kochbuch kein Rezept, wie man es richtig macht. Später wurde dieses Problem behoben, aber Sie könnten interessiert sein zu sehen, wie Tests in meinem Projekt funktionieren.
Das Kochbuch enthält ein einfaches Rezept für den genauen Start des Generators. Sie können es gegen einen Quellcode spielen und sicherstellen, dass die Generierung fehlerfrei abgeschlossen wird. Und dann stellt sich die Frage: Wie kann sichergestellt werden, dass der Code korrekt erstellt wird und ordnungsgemäß funktioniert? Sie können natürlich einen Referenzcode verwenden, ihn mit CSharpSyntaxTree.ParseText analysieren und dann mit IsEquivalentTo vergleichen . Der Code ändert sich jedoch tendenziell, und der Vergleich mit dem Code, der funktional identisch ist, sich jedoch in Kommentaren und Leerzeichen unterscheidet, ergab ein negatives Ergebnis. Gehen wir den langen Weg:
Lassen Sie uns eine Zusammenstellung erstellen;
Lassen Sie uns einen Generator erstellen und ausführen.
Lassen Sie uns die Bibliothek erstellen und in den aktuellen Prozess laden.
Lassen Sie uns den resultierenden Code dort finden und ausführen.
Zusammenstellung
Der Compiler wird mit der Funktion CSharpCompilation.Create gestartet . Hier können Sie Code hinzufügen und Links zu Bibliotheken einfügen. Der Quellcode wird mit CSharpSyntaxTree.ParseText und den MetadataReference.CreateFromFile- Bibliotheken erstellt (es gibt Optionen für Streams und Arrays). Wie komme ich an den Weg? In den meisten Fällen ist alles einfach:
typeof(UnresolvedType).Assembly.Location
In einigen Fällen befindet sich der Typ jedoch in der Referenzbaugruppe. Dann funktioniert dies:
Assembly.Load(new AssemblyName("System.Linq.Expressions")).Location
Assembly.Load(new AssemblyName("System.Runtime")).Location
Assembly.Load(new AssemblyName("netstandard")).Location
Wie die Erstellung einer Zusammenstellung aussehen könnte
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));
Starten des Generators und Erstellen der Baugruppe
: 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. .
Der Scheingenerator ist hier . Dies ist eine Beta-Version und wird für die Verwendung in der Produktion nicht empfohlen.