头图

Hello everyone, I am Li Weihan, a laboratory researcher in this issue. Today I will show you how to perform unit testing based on Source Generator. Next, let us go to the laboratory to find out!

image.png

Source Generator unit test

Intro

Source Generator is a mechanism for dynamically generating code during compilation introduced after .NET 5.0. For introduction, please refer to C# Powerful new features Source Generator , but Source Generator testing has been troublesome for a long time. , It will be more troublesome to write unit tests to verify. I participated in a Source Generator related project a few days ago and found that Microsoft now provides a set of test components to simplify Source Generator unit testing. Today we will introduce two Source Generator examples. Just use it.

GetStarted

It is relatively simple to use. I usually use xunit, so the following examples also use xunit to write unit tests. The test components provided by Microsoft are also for MsTest and NUnit. You can choose according to your needs.

https://www.nuget.org/packages?q=Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing

0c3b653a5e58dd3f86e219a08fed6773.png

My project is xunit, so first need to be referenced in the test project
Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit the NuGet package 061c1352d2d00b, if it is not xunit, just select the corresponding NuGet package.

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

Sample1

Let's first look at one of the simplest Source Generator examples:

[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 HelloGenerator . There is only one Test 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 source generator test is divided into two parts, one is the source code, and the other is the code generated by the Generator.

And this example is relatively simple, in fact, it has nothing to do with the source code, there is no source code, the above is a blank, or you do not need to configure Sources

And Generated Sources is the code generated by our Generator.

First, we need to create a CSharpSourceGeneratorTest with two generic types, the first is the Generator type, the second is the validator, which is related to which test framework you use, xunit is fixed to XUnitVerifier , specify TestState Source code and generated source code, then call the 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 AddSource in Generator, but it should be noted here that even if the specified name is not the .cs , you need to add the .cs here. This place feels like it can be optimized, and the suffix .cs

The third parameter is the actual generated code.

Sample2

Next, let's look at a slightly more complicated one, which 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();
    }

It's roughly the same as the previous example. The big difference is that dependencies need to be processed here. There are three processing methods provided in the above code. The WithPackages method only supports the NuGet package method. If the dll is directly referenced, the first two can be used. Way to achieve.

More

In the previous introduction article, we recommend adding a sentence of Debugger.Launch() to the code to debug the Source Generator. After unit testing, we don’t need this. Debug our test cases can also debug our Generator. In many cases it will It is more convenient, and it will be more efficient if you don’t need to trigger the Debugger when compiling. There can be less magical Debugger.Launch() in the code. It is more recommended to use unit testing to test the Generator.

The handling of the second example dependency above, I stepped on a lot of pits, and tried many times by myself, but it didn't work. Google/StackOverflow is great.

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

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

https://github.com/WeihanLi/SamplesInPractice/blob/master/SourceGeneratorSample/SourceGeneratorTest/GeneratorTest.cs

Reference

Microsoft Most Valuable Professional (MVP)

bc93fde364ea9dd3d9106b58e805b770.png

Microsoft's Most Valuable Expert is a global award granted by Microsoft to third-party technology professionals. For 28 years, technology community leaders around the world have won this award for sharing their expertise and experience in online and offline technology communities.

MVP is a team of experts who have been carefully selected. They represent the most skilled and intelligent people, and they are experts who have great enthusiasm for the community and are willing to help others. MVP is committed to helping others through speeches, forum questions and answers, creating websites, writing blogs, sharing videos, open source projects, organizing conferences, etc., and to help 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 follow the Microsoft China MSDN subscription account for more latest releases!
image.png


微软技术栈
423 声望997 粉丝

微软技术生态官方平台。予力众生,成就不凡!微软致力于用技术改变世界,助力企业实现数字化转型。