大家好,我是本期的實驗室研究員——李衛涵。今天我將向大家介紹如何基於針對 Source Generator 來進行單元測試。接下來就讓我們一起到實驗室中一探究竟吧!
Source Generator 單元測試
Intro
Source Generator 是 .NET 5.0 以後引入的一個在編譯期間動態生成程式碼的一個機制,介紹可以參考 C# 強大的新特性 Source Generator,但是很長時間以來 Source Generator 的測試都是有一些麻煩的,寫單元測試來驗證會比較麻煩,前幾天參與了一個 Source Generator 相關的專案,發現微軟現在有提供一套用於簡化 Source Generator 單元測試的測試元件,今天我們就以兩個 Source Generator 示例來介紹一下使用。
GetStarted
使用起來還算比較簡單的,我平時一般用 xunit,所以下面的示例也是使用 xunit 來寫單元測試,微軟提供的測試元件也有針對 MsTest 和 NUnit 的,可以根據自己需要進行選擇。
https://www.nuget.org/package...
我的專案是 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.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();
}
大體上和前面的示例差不多,比較大的差異在於,這裡需要處理依賴項,上面程式碼中提供的三種處理方式,其中 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/S...
參考資料
- https://stackoverflow.com/que...
- https://www.thinktecture.com/...
- https://github.com/dotnet/ros...
- https://github.com/dotnet/ros...
- https://www.nuget.org/package...
- https://github.com/WeihanLi/S...
- C# 強大的新特性 Source Generator
- 使用 Source Generator 代替 T4 動態生成程式碼
- 使用 Source Generator 自動生成 WEB API
微軟最有價值專家(MVP)
微軟最有價值專家是微軟公司授予第三方技術專業人士的一個全球獎項。28年來,世界各地的技術社群領導者,因其線上上和線下的技術社群中分享專業知識和經驗而獲得此獎項。
MVP是經過嚴格挑選的專家團隊,他們代表著技術最精湛且最具智慧的人,是對社群投入極大的熱情並樂於助人的專家。MVP致力於通過演講、論壇問答、建立網站、撰寫部落格、分享視訊、開源專案、組織會議等方式來幫助他人,並最大程度地幫助微軟技術社群使用者使用Microsoft技術。
更多詳情請登入官方網站:
https://mvp.microsoft.com/zh-cn
歡迎關注微軟中國MSDN訂閱號,獲取更多最新發布!