测试最佳实践
本文档介绍源生成器的测试最佳实践,包括单元测试、快照测试和性能测试。
📋 文档信息
| 属性 | 值 |
|---|---|
| 难度 | 中级 - 高级 |
| 阅读时间 | 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}");
}
}
}💡 关键要点
- 单元测试 - 测试单个功能和边界情况
- 快照测试 - 验证生成的代码格式
- 集成测试 - 测试完整的生成流程
- 性能测试 - 测量执行时间和内存使用
- 测试辅助工具 - 创建可复用的测试工具
- 使用 BenchmarkDotNet - 进行专业的性能测试
- 验证诊断 - 测试错误和警告消息
🔗 相关资源
📖 下一步
返回最佳实践概览 → 最佳实践概览
最后更新: 2025-01-21文档版本: 1.0