Skip to content

测试最佳实践

本文档介绍源生成器的测试最佳实践,包括单元测试、快照测试和性能测试。


📋 文档信息

属性
难度中级 - 高级
阅读时间20 分钟
前置知识单元测试、xUnit/NUnit

🎯 学习目标

学完本文档后,你将能够:

  • ✅ 编写单元测试验证生成器功能
  • ✅ 使用快照测试验证生成的代码
  • ✅ 实施集成测试
  • ✅ 进行性能测试

🧪 单元测试

为生成器编写单元测试

✅ 推荐:完整的单元测试

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;

public class JsonSerializerGeneratorTests
{
    [Fact]
    public void Generator_GeneratesSerializerForSimpleClass()
    {
        // Arrange
        var source = @"
using System;

[JsonSerializable]
public partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}";
        
        // Act
        var (compilation, diagnostics) = RunGenerator(source);
        
        // Assert
        Assert.Empty(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
        
        var generatedCode = GetGeneratedCode(compilation, "Person.g.cs");
        Assert.Contains("public string ToJson()", generatedCode);
        Assert.Contains("public static Person FromJson(string json)", generatedCode);
    }
    
    [Fact]
    public void Generator_ReportsError_ForNonPartialClass()
    {
        // Arrange
        var source = @"
[JsonSerializable]
public class Person  // 不是 partial
{
    public string Name { get; set; }
}";
        
        // Act
        var (compilation, diagnostics) = RunGenerator(source);
        
        // Assert
        var error = diagnostics.FirstOrDefault(d => 
            d.Id == "JSG001" && 
            d.Severity == DiagnosticSeverity.Error);
        Assert.NotNull(error);
    }
    
    private (Compilation, ImmutableArray<Diagnostic>) RunGenerator(string source)
    {
        var syntaxTree = CSharpSyntaxTree.ParseText(source);
        var compilation = CreateCompilation(syntaxTree);
        
        var generator = new JsonSerializerGenerator();
        var driver = CSharpGeneratorDriver.Create(generator);
        
        driver = driver.RunGeneratorsAndUpdateCompilation(
            compilation,
            out var outputCompilation,
            out var diagnostics);
        
        return (outputCompilation, diagnostics);
    }
    
    private Compilation CreateCompilation(SyntaxTree syntaxTree)
    {
        var references = new[]
        {
            MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
            MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location),
        };
        
        return CSharpCompilation.Create(
            "TestAssembly",
            new[] { syntaxTree },
            references,
            new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
    }
    
    private string GetGeneratedCode(Compilation compilation, string fileName)
    {
        var generatedTree = compilation.SyntaxTrees
            .FirstOrDefault(t => t.FilePath.EndsWith(fileName));
        
        return generatedTree?.ToString() ?? string.Empty;
    }
}

📸 快照测试

使用快照测试验证生成的代码

✅ 推荐:快照测试

csharp
public class SnapshotTests
{
    [Fact]
    public void Generator_GeneratesExpectedCode_ForComplexClass()
    {
        // Arrange
        var source = @"
[JsonSerializable]
public partial class Order
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
    public decimal TotalAmount { get; set; }
    public DateTime OrderDate { get; set; }
}";
        
        var expectedSnapshot = @"
// <auto-generated />

namespace TestNamespace
{
    public partial class Order
    {
        // Generated properties
        public string ToJson()
        {
            var sb = new System.Text.StringBuilder();
            sb.Append(""{"");
            sb.Append(""\"Id\":"");
            sb.Append(System.Text.Json.JsonSerializer.Serialize(Id));
            sb.Append("","");
            sb.Append(""\"CustomerName\":"");
            sb.Append(System.Text.Json.JsonSerializer.Serialize(CustomerName));
            sb.Append("","");
            sb.Append(""\"TotalAmount\":"");
            sb.Append(System.Text.Json.JsonSerializer.Serialize(TotalAmount));
            sb.Append("","");
            sb.Append(""\"OrderDate\":"");
            sb.Append(System.Text.Json.JsonSerializer.Serialize(OrderDate));
            sb.Append(""}"");
            return sb.ToString();
        }
        
        public static Order FromJson(string json)
        {
            return System.Text.Json.JsonSerializer.Deserialize<Order>(json);
        }
    }
}";
        
        // Act
        var (compilation, _) = RunGenerator(source);
        var generatedCode = GetGeneratedCode(compilation, "Order.g.cs");
        
        // Assert
        var normalizedGenerated = NormalizeCode(generatedCode);
        var normalizedExpected = NormalizeCode(expectedSnapshot);
        
        Assert.Equal(normalizedExpected, normalizedGenerated);
    }
    
    private string NormalizeCode(string code)
    {
        // 移除空白字符差异
        return CSharpSyntaxTree.ParseText(code)
            .GetRoot()
            .NormalizeWhitespace()
            .ToFullString();
    }
}

🔗 集成测试

测试完整的生成流程

✅ 推荐:集成测试

csharp
public class IntegrationTests
{
    [Fact]
    public void Generator_WorksWithMultipleClasses()
    {
        // Arrange
        var source = @"
[JsonSerializable]
public partial class Person
{
    public string Name { get; set; }
}

[JsonSerializable]
public partial class Address
{
    public string Street { get; set; }
}";
        
        // Act
        var (compilation, diagnostics) = RunGenerator(source);
        
        // Assert
        Assert.Empty(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
        
        var personCode = GetGeneratedCode(compilation, "Person.g.cs");
        var addressCode = GetGeneratedCode(compilation, "Address.g.cs");
        
        Assert.NotEmpty(personCode);
        Assert.NotEmpty(addressCode);
    }
}

⚡ 性能测试

测量生成器的执行时间

✅ 推荐:性能测试

csharp
using System.Diagnostics;

public class PerformanceTests
{
    [Fact]
    public void Generator_Performance_Test()
    {
        // Arrange
        var source = GenerateLargeSource(1000); // 生成大型源代码
        
        // Act
        var stopwatch = Stopwatch.StartNew();
        var (compilation, diagnostics) = RunGenerator(source);
        stopwatch.Stop();
        
        // Assert
        Assert.True(stopwatch.ElapsedMilliseconds < 5000, 
            $"Generator took {stopwatch.ElapsedMilliseconds}ms");
        
        // 验证没有错误
        Assert.Empty(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
    }
    
    private string GenerateLargeSource(int classCount)
    {
        var sb = new StringBuilder();
        
        for (int i = 0; i < classCount; i++)
        {
            sb.AppendLine($@"
[JsonSerializable]
public partial class Class{i}
{{
    public string Property1 {{ get; set; }}
    public int Property2 {{ get; set; }}
}}");
        }
        
        return sb.ToString();
    }
}

🎯 使用 BenchmarkDotNet 进行性能测试

专业的性能测试

✅ 推荐:使用 BenchmarkDotNet

csharp
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
public class GeneratorBenchmarks
{
    private Compilation _compilation;
    private MyGenerator _generator;
    
    [GlobalSetup]
    public void Setup()
    {
        _compilation = CreateCompilation();
        _generator = new MyGenerator();
    }
    
    [Benchmark]
    public void RunGenerator()
    {
        var driver = CSharpGeneratorDriver.Create(_generator);
        driver.RunGenerators(_compilation);
    }
    
    [Benchmark]
    public void RunGenerator_WithLargeInput()
    {
        var largeCompilation = CreateLargeCompilation();
        var driver = CSharpGeneratorDriver.Create(_generator);
        driver.RunGenerators(largeCompilation);
    }
}

// 运行基准测试
public class Program
{
    public static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<GeneratorBenchmarks>();
    }
}

🧩 测试辅助工具

创建测试辅助类

✅ 推荐:测试辅助类

csharp
public static class GeneratorTestHelpers
{
    public static (Compilation, ImmutableArray<Diagnostic>) RunGenerator<T>(
        string source,
        params MetadataReference[] additionalReferences)
        where T : IIncrementalGenerator, new()
    {
        var syntaxTree = CSharpSyntaxTree.ParseText(source);
        var compilation = CreateCompilation(syntaxTree, additionalReferences);
        
        var generator = new T();
        var driver = CSharpGeneratorDriver.Create(generator);
        
        driver = driver.RunGeneratorsAndUpdateCompilation(
            compilation,
            out var outputCompilation,
            out var diagnostics);
        
        return (outputCompilation, diagnostics);
    }
    
    public static Compilation CreateCompilation(
        SyntaxTree syntaxTree,
        params MetadataReference[] additionalReferences)
    {
        var references = new List<MetadataReference>
        {
            MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
            MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location),
        };
        
        references.AddRange(additionalReferences);
        
        return CSharpCompilation.Create(
            "TestAssembly",
            new[] { syntaxTree },
            references,
            new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
    }
    
    public static string GetGeneratedCode(Compilation compilation, string fileName)
    {
        var generatedTree = compilation.SyntaxTrees
            .FirstOrDefault(t => t.FilePath.EndsWith(fileName));
        
        return generatedTree?.ToString() ?? string.Empty;
    }
    
    public static void AssertNoErrors(ImmutableArray<Diagnostic> diagnostics)
    {
        var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList();
        
        if (errors.Any())
        {
            var errorMessages = string.Join("\n", errors.Select(e => e.GetMessage()));
            throw new Exception($"Compilation errors:\n{errorMessages}");
        }
    }
}

💡 关键要点

  1. 单元测试 - 测试单个功能和边界情况
  2. 快照测试 - 验证生成的代码格式
  3. 集成测试 - 测试完整的生成流程
  4. 性能测试 - 测量执行时间和内存使用
  5. 测试辅助工具 - 创建可复用的测试工具
  6. 使用 BenchmarkDotNet - 进行专业的性能测试
  7. 验证诊断 - 测试错误和警告消息

🔗 相关资源


📖 下一步

返回最佳实践概览 → 最佳实践概览


最后更新: 2025-01-21文档版本: 1.0

基于 MIT 许可发布