大家好,我是本期的实验室研究员——李卫涵。明天我将向大家介绍如何基于针对 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 订阅号,获取更多最新公布!