Skip to content

Instantly share code, notes, and snippets.

@jeppevammenkristensen
Last active May 14, 2020 21:36
Show Gist options
  • Save jeppevammenkristensen/6d71be0a783f73e19abb8b9404c4ab41 to your computer and use it in GitHub Desktop.
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)
/// <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