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!
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
My project is xunit, so first need to be referenced in the test projectMicrosoft.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:
Reference
- https://stackoverflow.com/questions/65550409/adding-reference-assemblies-to-roslyn-analyzer-code-fix-unit-tests
- https://www.thinktecture.com/en/net/roslyn-source-generators-analyzers-code-fixes-testing/
- https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md#unit-testing-of-generators
- https://github.com/dotnet/roslyn-sdk/tree/main/src/Microsoft.CodeAnalysis.Testing
- https://www.nuget.org/packages/Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit/
- https://github.com/WeihanLi/SamplesInPractice/blob/master/SourceGeneratorSample/SourceGeneratorTest/GeneratorTest.cs
- C# Powerful new features Source Generator
- Use Source Generator instead of T4 to dynamically generate code
- Use Source Generator to automatically generate WEB API
Microsoft Most Valuable Professional (MVP)
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!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。