Source Generator 单元测试
Source Generator 單元測試
Intro
Source Generator 是 .NET 5.0 以后引入的一個在編譯期間動態生成代碼的一個機制,介紹可以參考 C# 強大的新特性 Source Generator
GetStarted
使用起來還算比較簡單的,我平時一般用 xunit,所以下面的示例也是使用 xunit 來寫單元測試,微軟提供的測試組件也有針對 MsTest 和 NUnit 的,可以根據自己需要進行選擇 https://www.nuget.org/packages?q=Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing
我的項目是 xunit , 所以首先需要在測試項目中引用 Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit 這個 NuGet 包,如果不是 xunit 選擇對應的 NuGet 包即可
如果在還原包的時候有包版本的警告可以顯式指定對應包的版本來消除警告
Sample1
首先來看一個最簡單的 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);} }這個 Source Generator 就是一個比較簡單的生成一個 HelloGenerator 的類,這個類里只有一個 Test 的靜態方法,單元測試方法如下:
[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(); }通常來說 Source Generator 的測試分為兩部分,一部分是源代碼,一部分 Generator 生成的代碼
而這個示例比較簡單,其實和源代碼沒有關系,可以沒有源代碼,上面是給了一個空,也可以不配置 Sources
而 Generated Sources 則是由我們的 Generator 生成的代碼
首先我們需要創建一個 CSharpSourceGeneratorTest 有兩個泛型類型,第一個是 Generator 類型,第二個是驗證器,這和你使用哪個測試框架有關系,xunit 就固定是 XUnitVerifier ,在 test 中指定 TestState 中的源代碼和生成的源代碼,之后調用 RunAsync 方法就可以了
上面有一個生成的示例,
第一個參數是 Generator 的類型,會根據 Generator 的類型獲取生成代碼的位置,
第二個參數是在 Generator 里 AddSource 時指定的名稱,但是這里需要注意的是,即使指定的名稱不是 .cs 結尾的需要也需要在這里添加 .cs 后綴,這個地方感覺可以優化一下,自動加 .cs 后綴
第三個參數就是實際生成的代碼了
Sample2
接著我們來看一個稍微復雜一些的,和源代碼有關系并且有依賴項
Generator 定義如下:
[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);}} }單元測試方法如下:
[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.AdditionalReferencestester.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();}大體上和前面的示例差不多,比較大的差異在于,這里需要處理依賴項,上面代碼中提供的三種處理方式,其中 WithPackages 方式只支持 NuGet 包方式,如果是直接引用的 dll 可以使用前面兩種方式來實現
More
在之前的介紹文章中我們推薦在代碼里添加一句 Debugger.Launch() 來調試 Source Generator,而有了單元測試之后,我們就可以不需要這個了,debug 我們的測試用例也可以調試我們的 Generator,很多時候就會比較方便,也不需要編譯的時候觸發選擇 Debugger 了會更加高效一些,代碼里可以少一些神奇的 Debugger.Launch() 了,更加推薦使用單元測試的方式來測試 Generator
上面的第二個示例依賴項的處理,踩了好多坑,自己試了好多次都不行,Google/StackOverflow 大法好
除了上面的 WithXxx 方式,我們還可以用 AddXxx 方式,Add 是增量的方式,而 With 是完全的替換掉對應的依賴
如果你的項目里也有用到 Source Generator,不妨試一下,上面示例的代碼可以從 Github 上獲取:https://github.com/WeihanLi/SamplesInPractice/blob/master/SourceGeneratorSample/SourceGeneratorTest/GeneratorTest.cs
References
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# 強大的新特性 Source Generator
使用 Source Generator 代替 T4 動態生成代碼
使用 Source Generator 自動生成 WEB API
總結
以上是生活随笔為你收集整理的Source Generator 单元测试的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 如何把 .NET 进程中的所有托管异常找
 - 下一篇: 不止命令行!自定义VS生成事件