第 7 步:测试调试
加载进度中...
📋 本步目标
- 学习如何编写单元测试验证生成器
- 掌握多种调试技巧
- 了解测试框架的使用
- 处理常见问题和故障排除
- 建立完整的测试策略
⏱️ 预计时间
约 2-3 小时
🎯 为什么需要测试?
源生成器是编译时运行的代码,错误会直接影响用户的编译过程。完善的测试可以:
- ✅ 确保正确性:验证生成的代码符合预期
- ✅ 防止回归:确保修改不会破坏现有功能
- ✅ 提高可维护性:清晰的测试用例作为文档
- ✅ 增强信心:在发布前发现问题
- ✅ 加速开发:快速验证修改
📚 测试策略
1. 快照测试(Snapshot Testing)
验证生成的代码内容与预期完全匹配。
csharp
[Fact]
public async Task Generator_Produces_Expected_Code()
{
var source = @"
using ToStringGenerator;
[GenerateToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}";
var expectedGenerated = @"
namespace TestNamespace
{
partial class Person
{
public override string ToString()
{
var sb = new System.Text.StringBuilder();
sb.Append(""Person { "");
sb.Append(""Name = "");
sb.Append(Name);
sb.Append("", Age = "");
sb.Append(Age);
sb.Append("" }"");
return sb.ToString();
}
}
}";
await VerifyGeneratedCode<ToStringGenerator>(source, expectedGenerated);
}2. 编译测试(Compilation Testing)
验证生成的代码可以成功编译。
csharp
[Fact]
public void Generated_Code_Compiles_Successfully()
{
// Arrange
var source = @"
using ToStringGenerator;
[GenerateToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}";
// Act
var compilation = CreateCompilation(source);
var generator = new ToStringGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGeneratorsAndUpdateCompilation(
compilation,
out var outputCompilation,
out var diagnostics);
// Assert
var errors = outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error);
Assert.Empty(errors);
}3. 诊断测试(Diagnostic Testing)
验证生成器正确报告错误和警告。
csharp
[Fact]
public void Generator_Reports_Error_For_Non_Partial_Class()
{
// Arrange
var source = @"
using ToStringGenerator;
[GenerateToString]
public class NonPartialClass // 缺少 partial
{
public string Name { get; set; }
}";
// Act
var diagnostics = GetGeneratorDiagnostics<ToStringGenerator>(source);
// Assert
Assert.Contains(diagnostics, d =>
d.Id == "TSG001" &&
d.Severity == DiagnosticSeverity.Error);
}4. 功能测试(Functional Testing)
验证生成的代码在运行时的行为。
csharp
[Fact]
public void Generated_ToString_Returns_Correct_Format()
{
// Arrange
var source = @"
using ToStringGenerator;
[GenerateToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}";
// Act
var assembly = CompileAndLoadAssembly(source);
var personType = assembly.GetType("Person");
var person = Activator.CreateInstance(personType);
personType.GetProperty("Name").SetValue(person, "Alice");
personType.GetProperty("Age").SetValue(person, 30);
var result = person.ToString();
// Assert
Assert.Contains("Alice", result);
Assert.Contains("30", result);
}🛠️ 设置测试项目
步骤 1:创建测试项目
bash
# 创建测试项目
dotnet new xunit -n ToStringGenerator.Tests
dotnet sln add ToStringGenerator.Tests
# 添加必要的包
cd ToStringGenerator.Tests
dotnet add package Microsoft.CodeAnalysis.CSharp
dotnet add package Microsoft.CodeAnalysis.CSharp.Workspaces
dotnet add reference ../ToStringGenerator.Generator步骤 2:配置测试项目
编辑 ToStringGenerator.Tests.csproj:
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ToStringGenerator.Generator\ToStringGenerator.Generator.csproj" />
</ItemGroup>
</Project>步骤 3:创建测试辅助类
创建 TestHelpers/CompilationHelper.cs:
csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Reflection;
namespace ToStringGenerator.Tests.TestHelpers;
public static class CompilationHelper
{
/// <summary>
/// 创建基础的 CSharpCompilation
/// </summary>
public static CSharpCompilation CreateCompilation(string source)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
// 添加必要的引用
var references = new[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
};
return CSharpCompilation.Create(
assemblyName: "TestAssembly",
syntaxTrees: new[] { syntaxTree },
references: references,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}
/// <summary>
/// 检查是否有编译错误
/// </summary>
public static bool HasErrors(Compilation compilation)
{
return compilation.GetDiagnostics()
.Any(d => d.Severity == DiagnosticSeverity.Error);
}
/// <summary>
/// 获取错误消息列表
/// </summary>
public static List<string> GetErrorMessages(Compilation compilation)
{
return compilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Select(d => d.GetMessage())
.ToList();
}
}创建 TestHelpers/GeneratorTestHelper.cs:
csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace ToStringGenerator.Tests.TestHelpers;
public static class GeneratorTestHelper
{
/// <summary>
/// 运行生成器并返回结果
/// </summary>
public static GeneratorDriverRunResult RunGenerator<TGenerator>(string source)
where TGenerator : IIncrementalGenerator, new()
{
var compilation = CompilationHelper.CreateCompilation(source);
var generator = new TGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGeneratorsAndUpdateCompilation(
compilation,
out var outputCompilation,
out var diagnostics);
return driver.GetRunResult();
}
/// <summary>
/// 运行生成器并获取生成的源代码
/// </summary>
public static string[] GetGeneratedSources<TGenerator>(string source)
where TGenerator : IIncrementalGenerator, new()
{
var result = RunGenerator<TGenerator>(source);
return result.GeneratedTrees
.Select(tree => tree.ToString())
.ToArray();
}
/// <summary>
/// 验证生成器是否生成了指定数量的文件
/// </summary>
public static bool VerifyGeneratedFileCount<TGenerator>(string source, int expectedCount)
where TGenerator : IIncrementalGenerator, new()
{
var result = RunGenerator<TGenerator>(source);
return result.GeneratedTrees.Length == expectedCount;
}
}📝 编写单元测试
测试 1:基本功能测试
创建 ToStringGeneratorTests.cs:
csharp
using Xunit;
using ToStringGenerator.Generator;
using ToStringGenerator.Tests.TestHelpers;
namespace ToStringGenerator.Tests;
public class ToStringGeneratorTests
{
[Fact]
public void Generator_Generates_ToString_Method()
{
// Arrange
var source = @"
using ToStringGenerator;
namespace TestNamespace
{
[GenerateToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}";
// Act
var generatedSources = GeneratorTestHelper
.GetGeneratedSources<ToStringGenerator>(source);
// Assert
Assert.NotEmpty(generatedSources);
Assert.Contains("public override string ToString()", generatedSources[0]);
Assert.Contains("Name", generatedSources[0]);
Assert.Contains("Age", generatedSources[0]);
}
[Fact]
public void Generator_Includes_All_Public_Properties()
{
// Arrange
var source = @"
using ToStringGenerator;
[GenerateToString]
public partial class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}";
// Act
var generatedSources = GeneratorTestHelper
.GetGeneratedSources<ToStringGenerator>(source);
var generatedCode = generatedSources[0];
// Assert
Assert.Contains("Name", generatedCode);
Assert.Contains("Price", generatedCode);
Assert.Contains("Stock", generatedCode);
}
[Fact]
public void Generator_Handles_Multiple_Classes()
{
// Arrange
var source = @"
using ToStringGenerator;
[GenerateToString]
public partial class Person
{
public string Name { get; set; }
}
[GenerateToString]
public partial class Product
{
public string Title { get; set; }
}";
// Act
var result = GeneratorTestHelper.RunGenerator<ToStringGenerator>(source);
// Assert
Assert.Equal(2, result.GeneratedTrees.Length);
}
}测试 2:边界情况测试
csharp
public class EdgeCaseTests
{
[Fact]
public void Generator_Handles_Empty_Class()
{
var source = @"
using ToStringGenerator;
[GenerateToString]
public partial class EmptyClass
{
}";
var generatedSources = GeneratorTestHelper
.GetGeneratedSources<ToStringGenerator>(source);
Assert.NotEmpty(generatedSources);
Assert.Contains("public override string ToString()", generatedSources[0]);
}
[Fact]
public void Generator_Ignores_Non_Partial_Class()
{
var source = @"
using ToStringGenerator;
[GenerateToString]
public class NonPartialClass
{
public string Name { get; set; }
}";
var result = GeneratorTestHelper.RunGenerator<ToStringGenerator>(source);
// 应该不生成代码或报告错误
Assert.True(
result.GeneratedTrees.Length == 0 ||
result.Diagnostics.Length > 0);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void Generator_Handles_Invalid_Input(string source)
{
var exception = Record.Exception(() =>
{
GeneratorTestHelper.RunGenerator<ToStringGenerator>(source ?? "");
});
// 不应该抛出异常
Assert.Null(exception);
}
}测试 3:编译测试
csharp
public class CompilationTests
{
[Fact]
public void Generated_Code_Compiles_Without_Errors()
{
var source = @"
using ToStringGenerator;
[GenerateToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}";
var compilation = CompilationHelper.CreateCompilation(source);
var generator = new ToStringGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGeneratorsAndUpdateCompilation(
compilation,
out var outputCompilation,
out _);
Assert.False(CompilationHelper.HasErrors(outputCompilation));
}
[Fact]
public void Generated_Code_Can_Be_Executed()
{
var source = @"
using ToStringGenerator;
[GenerateToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}";
var compilation = CompilationHelper.CreateCompilation(source);
var generator = new ToStringGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGeneratorsAndUpdateCompilation(
compilation,
out var outputCompilation,
out _);
// 编译并加载程序集
using var ms = new MemoryStream();
var emitResult = outputCompilation.Emit(ms);
Assert.True(emitResult.Success);
ms.Seek(0, SeekOrigin.Begin);
var assembly = Assembly.Load(ms.ToArray());
// 创建实例并调用生成的方法
var personType = assembly.GetType("Person");
var person = Activator.CreateInstance(personType);
personType.GetProperty("Name").SetValue(person, "Alice");
personType.GetProperty("Age").SetValue(person, 30);
var result = person.ToString();
Assert.Contains("Alice", result);
Assert.Contains("30", result);
}
}🐛 调试源生成器
方法 1:使用 Debugger.Launch()
这是最直接的调试方法:
csharp
[Generator]
public class ToStringGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
#if DEBUG
// 仅在 DEBUG 模式下启用
if (!System.Diagnostics.Debugger.IsAttached)
{
// 弹出调试器选择对话框
System.Diagnostics.Debugger.Launch();
}
#endif
// 生成器逻辑...
}
}使用步骤:
- 在生成器代码中添加
Debugger.Launch() - 构建使用者项目
- 会弹出调试器选择对话框
- 选择 Visual Studio 实例
- 设置断点并调试
优点:
- ✅ 简单直接
- ✅ 可以在生成器初始化时就开始调试
缺点:
- ❌ 每次编译都会弹出对话框
- ❌ 需要手动选择调试器
方法 2:附加到编译器进程
更灵活的调试方法:
步骤:
- 在 Visual Studio 中打开生成器项目
- 在生成器代码中设置断点
- 选择"调试" → "附加到进程"(Ctrl+Alt+P)
- 在进程列表中找到:
csc.exe- C# 编译器VBCSCompiler.exe- Roslyn 编译器服务器dotnet.exe- .NET CLI
- 附加调试器
- 在使用者项目中触发重新编译
提示:
- 使用进程名称过滤快速找到目标进程
- 可以附加到多个进程
- 如果找不到进程,先编译一次使用者项目
方法 3:使用单元测试调试
最推荐的方法:
csharp
[Fact]
public void Debug_Generator_With_Specific_Input()
{
// Arrange
var source = @"
[GenerateToString]
public partial class Person
{
public string Name { get; set; }
}";
var compilation = CompilationHelper.CreateCompilation(source);
var generator = new ToStringGenerator();
// 在这里设置断点 ⬇️
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
// 按 F11 步进到生成器代码
driver = driver.RunGeneratorsAndUpdateCompilation(
compilation,
out var outputCompilation,
out var diagnostics);
// 检查结果
var result = driver.GetRunResult();
}优点:
- ✅ 可以精确控制输入
- ✅ 可以重复运行
- ✅ 可以快速迭代
- ✅ 不影响正常编译
方法 4:使用日志输出
当无法使用调试器时:
csharp
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterSourceOutput(
classDeclarations,
(spc, classDecl) =>
{
// 输出调试信息到文件
var logPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
"generator-debug.log");
File.AppendAllText(logPath,
$"Processing class: {classDecl.Identifier}\n");
// 生成代码...
});
}注意:
- ⚠️ 不要在生产代码中保留日志输出
- ⚠️ 日志文件可能会变得很大
- ⚠️ 文件 I/O 会影响性能
✅ 检查点
✅ 检查点
完成以下任务后,可以进入下一步:
📝 小测验
测试你的理解:
为什么需要测试源生成器?
查看答案
源生成器在编译时运行,错误会直接影响用户的编译过程。测试可以确保正确性、防止回归、提高可维护性,并在发布前发现问题。什么是快照测试?
查看答案
快照测试验证生成的代码内容与预期完全匹配。它保存生成代码的"快照",并在后续测试中比较新生成的代码是否与快照一致。如何调试源生成器?
查看答案
主要有四种方法:1) 使用 Debugger.Launch() 弹出调试器;2) 附加到编译器进程;3) 使用单元测试调试(推荐);4) 使用日志输出。
🎯 最佳实践
✅ 推荐做法
- 使用测试辅助类:简化测试代码
- 测试隔离:每个测试独立运行
- 描述性命名:清晰描述测试意图
- 测试边界情况:空输入、特殊字符等
- 验证编译:确保生成的代码可以编译
- 使用单元测试调试:最高效的调试方法
❌ 避免做法
- 在生产代码中保留调试代码:使用条件编译
- 测试依赖外部状态:在测试中提供所有输入
- 忽略诊断信息:总是检查警告和错误
- 测试过于脆弱:使用规范化比较
💡 故障排除
问题 1:测试运行很慢
解决方案:
- 并行运行测试
- 使用测试分类
- 缓存编译对象
问题 2:找不到生成的代码
检查:
- 查看
obj/Debug/netX.X/generated/目录 - 确认生成器正确运行
- 检查项目引用配置
问题 3:调试器无法附加
解决方案:
- 确保编译器进程正在运行
- 尝试先编译一次使用者项目
- 使用单元测试调试
🎯 下一步
恭喜!你已经掌握了源生成器的测试和调试技巧。现在你知道了:
- ✅ 如何编写单元测试
- ✅ 多种测试策略
- ✅ 调试源生成器的方法
- ✅ 故障排除技巧
在下一步中,我们将学习如何将源生成器应用到生产环境。