Last active
May 14, 2020 21:36
-
-
Save jeppevammenkristensen/6d71be0a783f73e19abb8b9404c4ab41 to your computer and use it in GitHub Desktop.
Dotnet Source Generator to create an InitSubject method in a UnitTest (that will have defined and initalised parameters for the constructor)
This file contains hidden or 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
/// <summary> | |
/// Very opinionated Source generator that tries to match a class to test and generates a InitSubject method | |
/// that can return the class being tested. If there are parameters those are initialized with mocking setup | |
/// (using NSubstitute) and injected. The parameters will be exposed as fields | |
/// </summary> | |
[Generator] | |
public class UnitTestEnricher : ISourceGenerator | |
{ | |
public void Execute(SourceGeneratorContext context) | |
{ | |
var syntaxReceiver = (UnitTestSyntaxReceiver)context.SyntaxReceiver; | |
// See if we matched a class ending with Tests | |
var cls = syntaxReceiver.ClassToAugment; | |
if (cls is null) return; | |
// Get the semanticmodel for class syntaxtree and get the namedtypesymbol | |
var semanticModel = context.Compilation.GetSemanticModel(cls.SyntaxTree); | |
var namedType = semanticModel.GetDeclaredSymbol(cls) as INamedTypeSymbol; | |
// Strip the text Tests from the namespace and class and search to see if there is a match | |
var newNameSpace = namedType.ContainingNamespace.ToString().Replace(".Tests", ""); | |
var newClassName = namedType.Name.Replace("Tests", ""); | |
var original = context.Compilation.GetTypeByMetadataName($"{newNameSpace}.{newClassName}"); | |
// If a match is found. Generate the additional source | |
if (original != null) | |
{ | |
EnrichTestClass(context, namedType, original); | |
} | |
} | |
private static void EnrichTestClass(SourceGeneratorContext context, INamedTypeSymbol namedType, INamedTypeSymbol original) | |
{ | |
// Match the first constructor and get the parameters | |
var constructor = original.Constructors.First(); | |
var parameters = constructor.Parameters; | |
var stringBuilder = new StringBuilder(); | |
// add using statements. This poc is very opiniated so it force you to use NSubstitute. And then loop through | |
// unique namespaces and adds them | |
stringBuilder.AppendLine("using NSubstitute;"); | |
foreach (var item in parameters.Select(x => x.Type.ContainingNamespace.ToString()).Distinct()) | |
{ | |
// | |
stringBuilder.AppendLine($"using {item};"); | |
} | |
stringBuilder.AppendLine(); | |
// declare namespace and classname as the matched class ending with Tests as we are making a partial class version of that | |
stringBuilder.AppendLine($"namespace {namedType.ContainingNamespace}"); | |
stringBuilder.AppendLine("{"); | |
stringBuilder.AppendLine(); | |
stringBuilder.AppendLine($"\tpublic partial class {namedType.Name}"); | |
stringBuilder.AppendLine("\t{"); | |
// For all the parameters generate a local field, initialized with mocking. private {typename} _{parameterName} = Substitute.For<{typename}>(); | |
foreach (var item in parameters) | |
{ | |
stringBuilder.AppendLine($"\t\tprivate {item.Type.Name} _{item.Name} = Substitute.For<{item.Type.Name}>();"); | |
} | |
stringBuilder.AppendLine(); | |
// declares the InitSubject that returns and instance of the subject to test with the parameters passed in | |
stringBuilder.AppendLine($"\t\tpublic {original.Name} InitSubject()"); | |
stringBuilder.AppendLine($"\t\t{{"); | |
stringBuilder.AppendLine($"\t\t\treturn new {original.Name}("); | |
// Loop through the parameters and "inject" them in the constructor | |
foreach (var (item, i) in parameters.Select((x,i)=> (x,i))) | |
{ | |
if (i != 0) | |
{ | |
stringBuilder.AppendLine(","); | |
} | |
stringBuilder.Append($"\t\t\t\t_{item.Name}"); | |
} | |
stringBuilder.AppendLine("\t\t\t);"); | |
stringBuilder.AppendLine("\t\t}"); | |
stringBuilder.AppendLine("\t}"); | |
stringBuilder.AppendLine("}"); | |
// Adds this text to the source | |
context.AddSource($"{namedType.Name}.generated.cs", SourceText.From(stringBuilder.ToString(), Encoding.UTF8)); | |
} | |
public void Initialize(InitializationContext context) | |
{ | |
// Register the receiver that will match the Class | |
context.RegisterForSyntaxNotifications(() => new UnitTestSyntaxReceiver()); | |
} | |
} | |
public class UnitTestSyntaxReceiver : ISyntaxReceiver | |
{ | |
public ClassDeclarationSyntax ClassToAugment { get; set; } | |
public void OnVisitSyntaxNode(SyntaxNode syntaxNode) | |
{ | |
// If we match a class with a name that ends with Tests, that is returned | |
if (syntaxNode is ClassDeclarationSyntax cds && cds.Identifier.Text.EndsWith("Tests")) | |
{ | |
ClassToAugment = cds; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment