測試與除錯 Aspects
測試和除錯編譯期程式碼需要與傳統執行期程式碼不同的方法。本章涵蓋所有可用的策略。
測試策略
Metalama 支援三種互補的測試方法:
| 策略 | 測試內容 | 是否執行程式碼? | 最適用於 |
|---|---|---|---|
| 快照測試 | 程式碼轉換正確性 | 否 | 驗證產生的程式碼結構 |
| 執行期測試 | 實際行為 | 是 | 驗證副作用和結果 |
| 編譯期單元測試 | 編譯期輔助方法 | 部分 | 複雜的編譯期邏輯 |
快照測試
快照測試將轉換後的輸出與基準檔案進行比較。如果 Aspect 變更了程式碼產生方式,測試就會失敗。
設定
- 新增測試框架套件:
dotnet add package Metalama.Testing.AspectTesting
- 建立具有以下結構的測試專案:
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 執行測試。框架會:
- 使用 Metalama 編譯輸入檔案
- 將轉換後的輸出與
.t.cs基準檔案進行比較 - 將差異報告為測試失敗
更新基準檔案
當你刻意變更 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 程式碼以兩種形式存在:
- 原始碼形式:你在 Aspect 類別中撰寫的內容(存在於編譯期)
- 轉換後形式:實際執行的內容(存在於
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:除錯轉換後的程式碼
- 建置專案
- 導覽至
obj/<Configuration>/<TFM>/metalama/ - 開啟轉換後的
.cs檔案 - 在轉換後的程式碼中設定中斷點
- 附加除錯器執行
策略 4:LamaDebug 組態
在 Visual Studio 中建立 LamaDebug 建置組態以便輕鬆除錯:
- 開啟專案屬性 → 建置組態
- 建立名為
LamaDebug的新組態 - 在專案檔中:
<PropertyGroup Condition="'$(Configuration)' == 'LamaDebug'">
<DefineConstants>DEBUG;TRACE;LAMADEBUG</DefineConstants>
<MetalamaDebugTransformedCode>True</MetalamaDebugTransformedCode>
</PropertyGroup>
- 除錯 Aspects 時切換到
LamaDebug組態 - 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=True | Debugger.Break() |
| Template 程式碼 | meta.DebugBreak() | meta.DebugBreak() |
| 轉換後的程式碼 | 在 obj/.../metalama/ 中設定中斷點 | 標準除錯器 |
| 建置輸出 | 診斷報告 | builder.Diagnostics.Report() |
下一篇:優勢與劣勢 — 何時使用(以及不使用)Metalama。