Source Generator unit test

Posted by headrush on Wed, 15 Dec 2021 11:03:29 +0100

Hello, I'm Li Weihan, a laboratory researcher in this issue. Today, I'll show you how to do unit testing based on Source Generator. Next, let's go to the lab and find out!

Source Generator unit test

Intro

The Source Generator is NET 5.0, a mechanism for dynamically generating code during compilation, which can be referred to for introduction C # powerful new feature Source Generator However, the testing of Source Generator has been troublesome for a long time. It will be troublesome to write unit tests for verification. I participated in a project related to Source Generator a few days ago and found that Microsoft now provides a set of test components to simplify the unit testing of Source Generator. Today we will introduce the use of two Source Generator examples.

GetStarted

It's relatively simple to use. I usually use xunit, so the following example also uses xunit to write unit tests. The test components provided by Microsoft are also for MsTest and NUnit, which can be selected according to their own needs.

https://www.nuget.org/package...

My project is xunit, so I need to reference it in the test project first
Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit is a NuGet package. If it is not xunit, select the corresponding NuGet package.

If there is a package version warning when restoring a package, you can explicitly specify the corresponding package version to eliminate the warning.

Sample1

First, let's take a look at the simplest example of Source Generator:

[Generator]
public class HelloGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // for debugging
        // if (!Debugger.IsAttached) Debugger.Launch();
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var code = @"namespace HelloGenerated
{
  public class HelloGenerator
  {
    public static void Test() => System.Console.WriteLine(""Hello Generator"");
  }
}";
        context.AddSource(nameof(HelloGenerator), code);
    }
}

This Source Generator is a relatively simple class that generates a HelloGenerator. There is only one static Test method in this class. The unit Test method is as follows:

[Fact]
public async Task HelloGeneratorTest()
{
    var code = string.Empty;
    var generatedCode = @"namespace HelloGenerated
{
  public class HelloGenerator
  {
    public static void Test() => System.Console.WriteLine(""Hello Generator"");
  }
}";
    var tester = new CSharpSourceGeneratorTest<HelloGenerator, XUnitVerifier>()
    {
        TestState =
            {
                Sources = { code },
                GeneratedSources =
                {
                    (typeof(HelloGenerator), $"{nameof(HelloGenerator)}.cs", SourceText.From(generatedCode, Encoding.UTF8)),
                }
            },
    };

    await tester.RunAsync();
}

Generally speaking, the test of Source Generator is divided into two parts, one is the source code and the other is the code generated by Generator.

This example is relatively simple. In fact, it has nothing to do with the source code. There can be no source code. The above is an empty one, or you can not configure Sources

Generated Sources is the code generated by our Generator.

First, we need to create a CSharpSourceGeneratorTest with two generic types, the first is Generator type, and the second is the validator, which is related to which test framework you use. xunit is fixed XUnitVerifier, specifying the source code and the generated source code in TestState, and then calling RunAsync method.

There is a generated example above,

The first parameter is the type of Generator. The location of the generated code will be obtained according to the type of Generator,

The second parameter is the name specified when AddSource is added in the Generator, but it should be noted here that even if the specified name is not The end of cs also needs to be added here cs suffix, this place can be optimized and added automatically cs suffix.

The third parameter is the actually generated code.

Sample2

Next, let's look at a slightly more complex one that is related to the source code and has dependencies.

Generator is defined as follows:

[Generator]
public class ModelGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // Debugger.Launch();
        context.RegisterForSyntaxNotifications(() => new CustomSyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var codeBuilder = new StringBuilder(@"
using System;
using WeihanLi.Extensions;

namespace Generated
{
  public class ModelGenerator
  {
    public static void Test()
    {
      Console.WriteLine(""-- ModelGenerator --"");
");

        if (context.SyntaxReceiver is CustomSyntaxReceiver syntaxReceiver)
        {
            foreach (var model in syntaxReceiver.Models)
            {
                codeBuilder.AppendLine($@"      ""{model.Identifier.ValueText} Generated"".Dump();");
            }
        }

        codeBuilder.AppendLine("    }");
        codeBuilder.AppendLine("  }");
        codeBuilder.AppendLine("}");
        var code = codeBuilder.ToString();
        context.AddSource(nameof(ModelGenerator), code);
    }
}

internal class CustomSyntaxReceiver : ISyntaxReceiver
{
    public List<ClassDeclarationSyntax> Models { get; } = new();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax)
        {
            Models.Add(classDeclarationSyntax);
        }
    }
}

The unit test method is as follows:

  [Fact]
    public async Task ModelGeneratorTest()
    {
        var code = @"
public class TestModel123{}
";
        var generatedCode = @"
using System;
using WeihanLi.Extensions;

namespace Generated
{
  public class ModelGenerator
  {
    public static void Test()
    {
      Console.WriteLine(""-- ModelGenerator --"");
      ""TestModel123 Generated"".Dump();
    }
  }
}
";
        var tester = new CSharpSourceGeneratorTest<ModelGenerator, XUnitVerifier>()
        {
            TestState =
                {
                    Sources = { code },
                    GeneratedSources =
                    {
                        (typeof(ModelGenerator), $"{nameof(ModelGenerator)}.cs", SourceText.From(generatedCode, Encoding.UTF8)),
                    }
                },
        };
        // references
        // TestState.AdditionalReferences
        tester.TestState.AdditionalReferences.Add(typeof(DependencyResolver).Assembly);

        // ReferenceAssemblies
        //    WithAssemblies
        //tester.ReferenceAssemblies = tester.ReferenceAssemblies
        //    .WithAssemblies(ImmutableArray.Create(new[] { typeof(DependencyResolver).Assembly.Location.Replace(".dll", "", System.StringComparison.OrdinalIgnoreCase) }))
        //    ;
        //    WithPackages
        //tester.ReferenceAssemblies = tester.ReferenceAssemblies
        //    .WithPackages(ImmutableArray.Create(new PackageIdentity[] { new PackageIdentity("WeihanLi.Common", "1.0.46") }))
        //    ;

        await tester.RunAsync();
    }

Generally, it is similar to the previous examples. The big difference is that dependencies need to be processed here. There are three processing methods provided in the above code, of which WithPackages only supports NuGet package. If it is a dll directly referenced, the first two methods can be used to implement it.

More

In the previous introduction article, we recommend adding a Debugger Launch() is used to debug the Source Generator. After unit testing, we can not need this. Debug our test cases can also debug our Generator. It is more convenient and does not need to be triggered during compilation. Selecting a Debugger will be more efficient, and there can be fewer magical debuggers in the code Launch(), and it is more recommended to use unit testing to test the Generator.

In the second example above, I stepped on a lot of holes in the processing of dependencies. I tried it many times, but it didn't work. Google/StackOverflow is a good method.

In addition to the WithXxx method above, we can also use the AddXxx method. Add is an incremental method, and With completely replaces the corresponding dependencies.

If Source Generator is also useful in your project, you might as well try it. The code of the above example can be obtained from Github:

https://github.com/WeihanLi/S...

reference material

Microsoft MVP

Microsoft's most valuable expert is a global award awarded by Microsoft to third-party technology professionals. Over the past 28 years, technology community leaders around the world have won this award for sharing expertise and experience in their online and offline technology communities.

MVP is a strictly selected expert team. They represent the most skilled and intelligent people. They are experts who are enthusiastic and helpful to the community. MVP is committed to helping others through lectures, forum Q & A, creating websites, writing blogs, sharing videos, open source projects, organizing meetings, and helping users in the Microsoft technology community use Microsoft technology to the greatest extent.
For more details, please visit the official website:
https://mvp.microsoft.com/zh-cn

Welcome to Microsoft China MSDN subscription number for more latest releases!

Topics: C# .NET Testing