Complete guide to xUnit test output and logging. Use when implementing test output, diagnostic logging, or ILogger substitutes in xUnit tests. Covers ITestOutputHelper injection, the AbstractLogger pattern, and structured output design. Includes implementations of XUnitLogger, CompositeLogger, and a performance-test diagnostics tool. Keywords: ITestOutputHelper, ILogger testing, test output xunit, test output, test logging, AbstractLogger, XUnitLogger, CompositeLogger, testOutputHelper.WriteLine, test diagnostics, logger mock, test logs, structured output, Received().Log
本技能協助您在 .NET xUnit 測試專案中實作高品質的測試輸出與記錄機制。
正確的注入方式
ITestOutputHelperpublic class MyTests
{
private readonly ITestOutputHelper _output;
public MyTests(ITestOutputHelper testOutputHelper)
{
_output = testOutputHelper;
}
}
常見錯誤
private static ITestOutputHelper _output建議的輸出結構
private void LogSection(string title)
{
_output.WriteLine($"\n=== {title} ===");
}
private void LogKeyValue(string key, object value)
{
_output.WriteLine($"{key}: {value}");
}
private void LogTimestamp(DateTime time)
{
_output.WriteLine($"執行時間: {time:yyyy-MM-dd HH:mm:ss.fff}");
}
輸出時機
挑戰:擴充方法無法直接 Mock
ILogger.LogError() 是擴充方法,NSubstitute 無法直接攔截。需要攔截底層的 Log<TState> 方法:
// ❌ 錯誤:直接 Mock 擴充方法會失敗
logger.Received().LogError(Arg.Any<string>());
// ✅ 正確:攔截底層方法
logger.Received().Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Is<object>(o => o.ToString().Contains("預期訊息")),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception, string>>()
);
解決方案:使用抽象層
建立 AbstractLogger<T> 來簡化測試:
public abstract class AbstractLogger<T> : ILogger<T>
{
public IDisposable BeginScope<TState>(TState state)
=> null;
public bool IsEnabled(LogLevel logLevel)
=> true;
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter)
{
Log(logLevel, exception, state?.ToString() ?? string.Empty);
}
public abstract void Log(LogLevel logLevel, Exception ex, string information);
}
測試時使用
var logger = Substitute.For<AbstractLogger<MyService>>();
// 現在可以簡單驗證
logger.Received().Log(LogLevel.Error, Arg.Any<Exception>(), Arg.Is<string>(s => s.Contains("錯誤訊息")));
XUnitLogger:將記錄導向測試輸出
public class XUnitLogger<T> : ILogger<T>
{
private readonly ITestOutputHelper _testOutputHelper;
public XUnitLogger(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> formatter)
{
var message = formatter(state, exception);
_testOutputHelper.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] [{logLevel}] [{typeof(T).Name}] {message}");
if (exception != null)
{
_testOutputHelper.WriteLine($"Exception: {exception}");
}
}
// 其他必要的介面實作...
}
CompositeLogger:同時支援驗證與輸出
public class CompositeLogger<T> : ILogger<T>
{
private readonly ILogger<T>[] _loggers;
public CompositeLogger(params ILogger<T>[] loggers)
{
_loggers = loggers;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> formatter)
{
foreach (var logger in _loggers)
{
logger.Log(logLevel, eventId, state, exception, formatter);
}
}
// 其他介面實作會委派給所有內部 logger...
}
使用方式
// 同時進行行為驗證與測試輸出
var mockLogger = Substitute.For<AbstractLogger<MyService>>();
var xunitLogger = new XUnitLogger<MyService>(_output);
var compositeLogger = new CompositeLogger<MyService>(mockLogger, xunitLogger);
var service = new MyService(compositeLogger);
[Fact]
public async Task ProcessLargeDataSet_效能測試()
{
// Arrange
var stopwatch = Stopwatch.StartNew();
var checkpoints = new List<(string Stage, TimeSpan Elapsed)>();
_output.WriteLine("開始處理大型資料集...");
// Act & Monitor
await processor.LoadData(dataSet);
checkpoints.Add(("資料載入", stopwatch.Elapsed));
_output.WriteLine($"資料載入完成: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");
await processor.ProcessData();
checkpoints.Add(("資料處理", stopwatch.Elapsed));
_output.WriteLine($"資料處理完成: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");
stopwatch.Stop();
// Assert & Report
_output.WriteLine("\n=== 效能報告 ===");
foreach (var (stage, elapsed) in checkpoints)
{
_output.WriteLine($"{stage}: {elapsed.TotalMilliseconds:F2} ms");
}
}
public abstract class DiagnosticTestBase
{
protected readonly ITestOutputHelper Output;
protected DiagnosticTestBase(ITestOutputHelper output)
{
Output = output;
}
protected void LogTestStart(string testName)
{
Output.WriteLine($"\n=== {testName} ===");
Output.WriteLine($"執行時間: {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");
}
protected void LogTestData(object data)
{
Output.WriteLine($"測試資料: {JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true })}");
}
protected void LogAssertionFailure(string field, object expected, object actual)
{
Output.WriteLine("\n=== 斷言失敗 ===");
Output.WriteLine($"欄位: {field}");
Output.WriteLine($"預期值: {expected}");
Output.WriteLine($"實際值: {actual}");
}
}
適當使用 ITestOutputHelper
Logger 測試策略
過度使用輸出 — 大量輸出會拖慢測試執行速度,且雜訊會淹沒有用資訊。只在複雜或易失敗的測試中加入輸出即可。記錄敏感資訊(密碼、金鑰)也是常見的安全風險。
硬編碼記錄驗證 — 驗證完整的記錄訊息字串容易因訊息微調而失敗,驗證呼叫次數則過度指定了內部實作。改為驗證記錄層級和關鍵字即可。
忽略生命週期 — ITestOutputHelper 的實例與測試方法綁定,在靜態方法或跨測試方法中使用會拋出例外。非同步測試中遺漏 await 也可能導致輸出遺失。
參考 templates/ 目錄下的完整範例:
itestoutputhelper-example.cs - ITestOutputHelper 使用範例ilogger-testing-example.cs - ILogger 測試策略範例diagnostic-tools.cs - XUnitLogger 與 CompositeLogger 實作本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
unit-test-fundamentals - 單元測試基礎xunit-project-setup - xUnit 專案設定nsubstitute-mocking - 測試替身與模擬在實作測試輸出與記錄時,確認以下檢查項目:
npx skills add kevintsengtw/dotnet-testing-测试输出与日志下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer