メインコンテンツまでスキップ

測試與除錯 Aspects

測試和除錯編譯期程式碼需要與傳統執行期程式碼不同的方法。本章涵蓋所有可用的策略。


測試策略

Metalama 支援三種互補的測試方法:

策略測試內容是否執行程式碼?最適用於
快照測試程式碼轉換正確性驗證產生的程式碼結構
執行期測試實際行為驗證副作用和結果
編譯期單元測試編譯期輔助方法部分複雜的編譯期邏輯

快照測試

快照測試將轉換後的輸出與基準檔案進行比較。如果 Aspect 變更了程式碼產生方式,測試就會失敗。

設定

  1. 新增測試框架套件:
dotnet add package Metalama.Testing.AspectTesting
  1. 建立具有以下結構的測試專案:
MyAspect.Tests/
├── LogTests/
│ ├── BasicLog.cs ← Input code
│ └── BasicLog.t.cs ← Expected output (baseline)
├── RetryTests/
│ ├── RetrySync.cs
│ ├── RetrySync.t.cs
│ ├── RetryAsync.cs
│ └── RetryAsync.t.cs
└── MyAspect.Tests.csproj

撰寫快照測試

輸入檔案 (BasicLog.cs):

using MyAspects;

public class TestTarget
{
[Log]
public int Add(int a, int b)
{
return a + b;
}
}

預期輸出 (BasicLog.t.cs):

using MyAspects;

public class TestTarget
{
[Log]
public int Add(int a, int b)
{
Console.WriteLine(">> Entering Add");
Console.WriteLine($" a = {a}");
Console.WriteLine($" b = {b}");
try
{
int result;
result = a + b;
Console.WriteLine($"<< Exiting Add with result: {result}");
return result;
}
catch (Exception ex)
{
Console.WriteLine($"!! Exception in Add: {ex.Message}");
throw;
}
}
}

執行快照測試

使用 dotnet test 執行測試。框架會:

  1. 使用 Metalama 編譯輸入檔案
  2. 將轉換後的輸出與 .t.cs 基準檔案進行比較
  3. 將差異報告為測試失敗

更新基準檔案

當你刻意變更 Aspect 的行為時:

# Regenerate all baselines
dotnet test -p:UpdateExpectedOutput=true

測試診斷訊息

測試 Aspect 是否產生預期的警告或錯誤:

// Input file (ErrorTest.cs):
public class TestTarget
{
[Cache] // Should produce error: void methods can't be cached
public void DoSomething() { }
}

預期輸出 (ErrorTest.t.cs) 包含診斷註解:

public class TestTarget
{
[Cache]
public void DoSomething() // Error MY001: Cannot cache void methods
{ }
}

執行期測試

執行期測試使用標準測試框架驗證 Aspect 轉換後程式碼的實際行為

設定

使用任何標準測試框架(xUnit、NUnit、MSTest):

// Using xUnit + FluentAssertions + NSubstitute (GST convention)
public class LogAttributeTests
{
[Fact]
public void Log_ShouldLogMethodEntry()
{
// Arrange
var logger = Substitute.For<ILoggerService>();
AspectServiceLocator.Initialize(
new ServiceCollection()
.AddSingleton(logger)
.BuildServiceProvider());

var service = new TestService();

// Act
service.DoWork();

// Assert
logger.Received(1).Debug(
Arg.Any<string>(),
Arg.Is<string>(s => s.Contains("Entering DoWork")));
}
}

public class TestService
{
[Log]
public void DoWork()
{
// Business logic
}
}

測試 GST Aspects

GST 框架在 GST.Core.Aspects.Tests 中有完整的執行期測試:

public class NotNullAttributeTests
{
[Fact]
public void NotNull_WhenNull_ThrowsArgumentNullException()
{
// Arrange
var service = new TestService();

// Act & Assert
var act = () => service.Process(null!);
act.Should().Throw<ArgumentNullException>()
.WithParameterName("input");
}

[Fact]
public void NotNull_WhenNotNull_Succeeds()
{
var service = new TestService();
var act = () => service.Process("valid");
act.Should().NotThrow();
}
}

public class TestService
{
public void Process([NotNull] string input)
{
// Only reached if input is not null
}
}

測試重試行為

public class RetryAttributeTests
{
[Fact]
public void Retry_ShouldRetryOnFailure()
{
var callCount = 0;
var service = new RetryTestService(() =>
{
callCount++;
if (callCount < 3)
throw new InvalidOperationException("Transient error");
});

service.UnstableMethod();

callCount.Should().Be(3); // Called 3 times (2 failures + 1 success)
}
}

測試快取

public class CacheAttributeTests
{
[Fact]
public void Cache_ShouldReturnCachedValue()
{
// Arrange
var cacheService = new MemoryCacheService();
AspectServiceLocator.Initialize(
new ServiceCollection()
.AddSingleton<ICacheService>(cacheService)
.AddSingleton<ICacheKeyGenerator, DefaultCacheKeyGenerator>()
.BuildServiceProvider());

var repository = new TestRepository();

// Act
var result1 = repository.GetById(1);
var result2 = repository.GetById(1); // Should hit cache

// Assert
repository.CallCount.Should().Be(1); // Only called once
result2.Should().Be(result1);
}
}

除錯 Aspects

挑戰

Aspect 程式碼以兩種形式存在:

  1. 原始碼形式:你在 Aspect 類別中撰寫的內容(存在於編譯期)
  2. 轉換後形式:實際執行的內容(存在於 obj/.../metalama/

你無法在原始碼形式中設定中斷點並期望它們在執行期被觸發。除錯器看到的是轉換後的形式。

策略 1:除錯編譯期程式碼

用於除錯 BuildAspect() 和 Fabric 程式碼:

public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// This breakpoint will pause the COMPILER
Debugger.Break();

// Your compile-time logic
var method = builder.Target;
// ...
}

然後使用以下指令建置:

dotnet build -p:MetalamaDebugCompiler=True -p:MetalamaConcurrentBuildEnabled=False

編譯器會暫停並要求你附加除錯器。

策略 2:除錯 Templates

用於除錯 Template 展開:

public override dynamic? OverrideMethod()
{
// This inserts a Debugger.Break() into the GENERATED code
meta.DebugBreak();

Console.WriteLine("Before");
var result = meta.Proceed();
Console.WriteLine("After");
return result;
}

重要:在 Templates 中使用 meta.DebugBreak()不要使用 Debugger.Break()。後者會被產生為始終中斷的執行期程式碼。

策略 3:除錯轉換後的程式碼

  1. 建置專案
  2. 導覽至 obj/<Configuration>/<TFM>/metalama/
  3. 開啟轉換後的 .cs 檔案
  4. 在轉換後的程式碼中設定中斷點
  5. 附加除錯器執行

策略 4:LamaDebug 組態

在 Visual Studio 中建立 LamaDebug 建置組態以便輕鬆除錯:

  1. 開啟專案屬性 → 建置組態
  2. 建立名為 LamaDebug 的新組態
  3. 在專案檔中:
<PropertyGroup Condition="'$(Configuration)' == 'LamaDebug'">
<DefineConstants>DEBUG;TRACE;LAMADEBUG</DefineConstants>
<MetalamaDebugTransformedCode>True</MetalamaDebugTransformedCode>
</PropertyGroup>
  1. 除錯 Aspects 時切換到 LamaDebug 組態
  2. F11(逐步執行)會進入轉換後的程式碼

策略 5:檢視產生的程式碼

即使不進行除錯,你也可以閱讀產生的程式碼:

# After building, check:
ls obj/Debug/net8.0/metalama/

# You'll see transformed versions of your source files
# Open them to understand what the aspect generated

除錯技巧

常見除錯情境

情境方法
Aspect 未套用檢查資格規則,檢查 Attribute 放置位置
產生的程式碼錯誤閱讀 obj/.../metalama/ 中的轉換後程式碼
Template 邏輯錯誤使用 meta.DebugBreak(),檢視產生的程式碼
BuildAspect 邏輯錯誤使用 Debugger.Break(),以 MetalamaDebugCompiler=True 建置
執行期行為錯誤直接除錯轉換後的程式碼
Aspect 順序錯誤檢查 [AspectOrder] Attribute,檢視產生的程式碼

從編譯期程式碼記錄日誌

你可以在編譯期間寫入診斷訊息:

public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// This appears in the build output
builder.Diagnostics.Report(
DiagnosticDefinition.Create("DBG001", Severity.Warning,
$"Processing method: {builder.Target.Name}")
.WithMessage($"Processing method: {builder.Target.Name}"));
}

檢查 Aspect 套用情況

使用 Metalama Transitive Graph 來查看哪些 Aspects 被套用到哪些宣告:

// In a fabric, you can enumerate all aspects:
public override void AmendProject(IProjectAmender amender)
{
amender.SelectMany(p => p.Types)
.SelectMany(t => t.Methods)
.Where(m => m.Attributes.Any(a => a.Type.Is(typeof(LogAttribute))))
.ForEach(m =>
{
// Log which methods have [Log]
Console.WriteLine($"[Log] applied to: {m.DeclaringType.Name}.{m.Name}");
});
}

GST 測試慣例

GST 框架遵循以下測試慣例:

慣例詳細內容
框架xUnit
斷言FluentAssertions
模擬NSubstitute
測試位置tests/GST.Core.Aspects.Tests/
命名{AspectName}Tests.cs
模式Arrange-Act-Assert

測試檔案結構

tests/GST.Core.Aspects.Tests/
├── Validation/
│ ├── NotNullAttributeTests.cs
│ ├── NotEmptyAttributeTests.cs
│ └── RangeAttributeTests.cs
├── Caching/
│ └── CacheAttributeTests.cs
├── Authorization/
│ └── AuthorizeAttributeTests.cs
├── Audit/
│ └── AuditAttributeTests.cs
└── Helpers/
└── TestServiceProvider.cs

摘要

測試類型測試內容方式時機
快照程式碼轉換.t.cs 基準檔案每次 Aspect 變更
執行期實際行為xUnit + mocks關鍵商業邏輯
編譯期輔助方法標準單元測試複雜的編譯期邏輯
除錯目標方法關鍵 API
BuildAspect()Debugger.Break() + MetalamaDebugCompiler=TrueDebugger.Break()
Template 程式碼meta.DebugBreak()meta.DebugBreak()
轉換後的程式碼obj/.../metalama/ 中設定中斷點標準除錯器
建置輸出診斷報告builder.Diagnostics.Report()

下一篇優勢與劣勢 — 何時使用(以及不使用)Metalama。