Skip to content

第 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
        
        // 生成器逻辑...
    }
}

使用步骤

  1. 在生成器代码中添加 Debugger.Launch()
  2. 构建使用者项目
  3. 会弹出调试器选择对话框
  4. 选择 Visual Studio 实例
  5. 设置断点并调试

优点

  • ✅ 简单直接
  • ✅ 可以在生成器初始化时就开始调试

缺点

  • ❌ 每次编译都会弹出对话框
  • ❌ 需要手动选择调试器

方法 2:附加到编译器进程

更灵活的调试方法:

步骤

  1. 在 Visual Studio 中打开生成器项目
  2. 在生成器代码中设置断点
  3. 选择"调试" → "附加到进程"(Ctrl+Alt+P)
  4. 在进程列表中找到:
    • csc.exe - C# 编译器
    • VBCSCompiler.exe - Roslyn 编译器服务器
    • dotnet.exe - .NET CLI
  5. 附加调试器
  6. 在使用者项目中触发重新编译

提示

  • 使用进程名称过滤快速找到目标进程
  • 可以附加到多个进程
  • 如果找不到进程,先编译一次使用者项目

方法 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. 为什么需要测试源生成器?

    查看答案 源生成器在编译时运行,错误会直接影响用户的编译过程。测试可以确保正确性、防止回归、提高可维护性,并在发布前发现问题。
  2. 什么是快照测试?

    查看答案 快照测试验证生成的代码内容与预期完全匹配。它保存生成代码的"快照",并在后续测试中比较新生成的代码是否与快照一致。
  3. 如何调试源生成器?

    查看答案 主要有四种方法:1) 使用 Debugger.Launch() 弹出调试器;2) 附加到编译器进程;3) 使用单元测试调试(推荐);4) 使用日志输出。

🎯 最佳实践

✅ 推荐做法

  1. 使用测试辅助类:简化测试代码
  2. 测试隔离:每个测试独立运行
  3. 描述性命名:清晰描述测试意图
  4. 测试边界情况:空输入、特殊字符等
  5. 验证编译:确保生成的代码可以编译
  6. 使用单元测试调试:最高效的调试方法

❌ 避免做法

  1. 在生产代码中保留调试代码:使用条件编译
  2. 测试依赖外部状态:在测试中提供所有输入
  3. 忽略诊断信息:总是检查警告和错误
  4. 测试过于脆弱:使用规范化比较

💡 故障排除

问题 1:测试运行很慢

解决方案

  • 并行运行测试
  • 使用测试分类
  • 缓存编译对象

问题 2:找不到生成的代码

检查

  • 查看 obj/Debug/netX.X/generated/ 目录
  • 确认生成器正确运行
  • 检查项目引用配置

问题 3:调试器无法附加

解决方案

  • 确保编译器进程正在运行
  • 尝试先编译一次使用者项目
  • 使用单元测试调试

🎯 下一步

恭喜!你已经掌握了源生成器的测试和调试技巧。现在你知道了:

  • ✅ 如何编写单元测试
  • ✅ 多种测试策略
  • ✅ 调试源生成器的方法
  • ✅ 故障排除技巧

在下一步中,我们将学习如何将源生成器应用到生产环境。

⏭️ 继续学习


📚 扩展阅读

基于 MIT 许可发布