如何测试依赖GeneratorExecutionContext的Source Generator扩展方法?
我正在编写一个Source Generator,需要读取.csproj文件。通过GeneratorExecutionContext.AnalyzerConfigOptions.GlobalOptions中的build_property.ProjectDir获取文件路径,并写了两个扩展方法:
public static class GeneratorInitializationContextExtensions { public static string GetProjectDirectory(this GeneratorExecutionContext context) { context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.ProjectDir", out string? projectDirectory); if (projectDirectory is null) throw new("Can't find project directory"); return projectDirectory; } public static string GetProjectFile(this GeneratorExecutionContext context) { string projectDirectory = context.GetProjectDirectory(); string[] files = Directory.GetFiles(projectDirectory); string projectFile = files.Single(f => f.EndsWith(".csproj")); return projectFile; } }
这些方法运行正常,但我想为它们添加测试覆盖。遗憾的是,我无法创建用于测试的GeneratorExecutionContext实例——它的唯一构造函数是内部的,且AnalyzerConfigOptionsProvider属性没有setter。请问该如何解决此问题?
解决方案
1. 拆分核心逻辑,解耦对GeneratorExecutionContext的依赖
最直接的方式是把业务逻辑从扩展方法中抽离,让核心功能不依赖GeneratorExecutionContext,这样就能单独测试:
public static class GeneratorInitializationContextExtensions { public static string GetProjectDirectory(this GeneratorExecutionContext context) { return GetProjectDirectoryFromGlobalOptions(context.AnalyzerConfigOptions.GlobalOptions); } public static string GetProjectFile(this GeneratorExecutionContext context) { string projectDirectory = context.GetProjectDirectory(); return FindProjectFileInDirectory(projectDirectory); } // 抽离出可直接测试的核心方法 internal static string GetProjectDirectoryFromGlobalOptions(IReadOnlyDictionary<string, string> globalOptions) { globalOptions.TryGetValue("build_property.ProjectDir", out string? projectDirectory); if (projectDirectory is null) throw new("Can't find project directory"); return projectDirectory; } internal static string FindProjectFileInDirectory(string projectDirectory) { string[] files = Directory.GetFiles(projectDirectory); string projectFile = files.Single(f => f.EndsWith(".csproj")); return projectFile; } }
测试时直接调用GetProjectDirectoryFromGlobalOptions和FindProjectFileInDirectory:
- 给前者传入模拟的键值对字典,验证路径提取逻辑
- 给后者传入预先创建好测试文件的目录,验证项目文件查找逻辑
完全不需要依赖GeneratorExecutionContext实例。
2. 使用Moq模拟上下文依赖
如果一定要测试完整的扩展方法,可以用Moq模拟GeneratorExecutionContext的AnalyzerConfigOptions属性:
先安装Moq包,然后编写测试代码:
[TestClass] public class GeneratorInitializationContextExtensionsTests { [TestMethod] public void GetProjectDirectory_ValidOptions_ReturnsCorrectPath() { // 模拟GlobalOptions字典 var mockGlobalOptions = new Dictionary<string, string> { {"build_property.ProjectDir", "C:\\TestProject\\"} }; // 模拟AnalyzerConfigOptionsProvider var mockConfigProvider = new Mock<AnalyzerConfigOptionsProvider>(); mockConfigProvider.Setup(p => p.GlobalOptions).Returns(mockGlobalOptions); // 模拟GeneratorExecutionContext实例 var mockContext = Mock.Of<GeneratorExecutionContext>(c => c.AnalyzerConfigOptions == mockConfigProvider.Object); // 调用扩展方法并断言结果 string result = mockContext.GetProjectDirectory(); Assert.AreEqual("C:\\TestProject\\", result); } }
注意:部分Roslyn版本可能对GeneratorExecutionContext的模拟有限制,如果此方法失效,优先使用第一种拆分逻辑的方案。
3. 使用Roslyn测试工具包模拟完整生成环境
如果需要测试完整的Source Generator流程,可以使用Microsoft.CodeAnalysis.Testing包,它能模拟真实的生成上下文:
[TestClass] public class GeneratorIntegrationTests { [TestMethod] public async Task GetProjectFile_ReturnsCorrectCsprojPath() { // 创建测试项目,指定目录和.csproj文件 var testProject = new TestProject { ProjectDir = "C:\\TestProject\\", Files = { ["TestProject.csproj"] = "<Project Sdk=\"Microsoft.NET.Sdk\"></Project>" } }; // 配置生成器测试 var test = new SourceGeneratorTest<YourGenerator, DefaultVerifier> { TestProject = testProject }; // 运行测试,生成器内可正常调用你的扩展方法 await test.RunAsync(); // 这里可以断言生成器的输出结果,或验证扩展方法的执行逻辑 } }
这种方法适合端到端测试生成器整体流程,但如果只是测试扩展方法的逻辑,第一种拆分方案更轻量高效。
内容的提问来源于stack exchange,提问作者ScottishTapWater

