Source: kevintsengtw/dotnet-testing-agent-skills (MIT). Ported into dotnet-agent-harness.
Test Output and Logging Expert Guide
This skill helps you implement high-quality test output and logging mechanisms in .NET xUnit test projects.
Applicable Scenarios
Use this skill when asked to perform the following tasks:
- Use ITestOutputHelper in xUnit tests to output diagnostic information
- Implement ILogger test alternatives (XUnitLogger)
- Create AbstractLogger or CompositeLogger patterns
- Design structured test output for debugging
- Implement performance test diagnostic tools
Core Principles
1. ITestOutputHelper Usage Principles
Correct Injection Method
- Inject
ITestOutputHelper via constructor
- Each test class instance bound to test method
- Cannot be shared between static methods or across test methods
public class MyTests
{
private readonly ITestOutputHelper _output;
public MyTests(ITestOutputHelper testOutputHelper)
{
_output = testOutputHelper;
}
}
```text
### Common Mistakes
- ❌ Static access: `private static ITestOutputHelper _output`
- ❌ Using without awaiting in async tests
- ❌ Attempting to use in Dispose method
### 2. Structured Output Format Design
### Recommended Output Structure
```csharp
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($"Execution Time: {time:yyyy-MM-dd HH:mm:ss.fff}");
}
```text
### Output Timing
- At test start: Log test setup and input data
- During execution: Log important state changes
- Before assertion: Log expected and actual values
- At test end: Log execution time and result summary
### 3. ILogger Testing Strategy
### Challenge: Extension Methods Cannot Be Directly Mocked
`ILogger.LogError()` is an extension method, NSubstitute cannot directly intercept. Need to intercept underlying `Log<TState>` method:
```csharp
// ❌ Wrong: Directly mocking extension method fails
logger.Received().LogError(Arg.Any<string>());
// ✅ Correct: Intercept underlying method
logger.Received().Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Is<object>(o => o.ToString().Contains("expected message")),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception, string>>()
);
```text
### Solution: Use Abstraction Layer
Create `AbstractLogger<T>` to simplify testing:
```csharp
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);
}
```text
### Using in Tests
```csharp
var logger = Substitute.For<AbstractLogger<MyService>>();
// Now can simply verify
logger.Received().Log(LogLevel.Error, Arg.Any<Exception>(), Arg.Is<string>(s => s.Contains("error message")));
```text
### 4. Diagnostic Tool Integration
### XUnitLogger: Direct Logs to Test Output
```csharp
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}");
}
}
// Other necessary interface implementations...
}
```text
### CompositeLogger: Support Both Verification and Output
```csharp
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);
}
}
// Other interface implementations delegate to all internal loggers...
}
```text
### Usage
```csharp
// Perform both behavior verification and test output
var mockLogger = Substitute.For<AbstractLogger<MyService>>();
var xunitLogger = new XUnitLogger<MyService>(_output);
var compositeLogger = new CompositeLogger<MyService>(mockLogger, xunitLogger);
var service = new MyService(compositeLogger);
```text
## Implementation Guide
### Timing Recording in Performance Tests
```csharp
[Fact]
public async Task ProcessLargeDataSet_Performance_Test()
{
// Arrange
var stopwatch = Stopwatch.StartNew();
var checkpoints = new List<(string Stage, TimeSpan Elapsed)>();
_output.WriteLine("Starting large dataset processing...");
// Act & Monitor
await processor.LoadData(dataSet);
checkpoints.Add(("Data Load", stopwatch.Elapsed));
_output.WriteLine($"Data load complete: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");
await processor.ProcessData();
checkpoints.Add(("Data Processing", stopwatch.Elapsed));
_output.WriteLine($"Data processing complete: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");
stopwatch.Stop();
// Assert & Report
_output.WriteLine("\n=== Performance Report ===");
foreach (var (stage, elapsed) in checkpoints)
{
_output.WriteLine($"{stage}: {elapsed.TotalMilliseconds:F2} ms");
}
}
```text
### Diagnostic Test Base Class
```csharp
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($"Execution Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");
}
protected void LogTestData(object data)
{
Output.WriteLine($"Test Data: {JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true })}");
}
protected void LogAssertionFailure(string field, object expected, object actual)
{
Output.WriteLine("\n=== Assertion Failure ===");
Output.WriteLine($"Field: {field}");
Output.WriteLine($"Expected: {expected}");
Output.WriteLine($"Actual: {actual}");
}
}
```text
## DO - Recommended Practices
1. **Appropriate Use of ITestOutputHelper**
- ✅ Log important steps in complex tests
- ✅ Adopt consistent structured output format
- ✅ Provide diagnostic information when tests fail
- ✅ Record timing points in performance tests
2. **Logger Testing Strategy**
- ✅ Use abstraction layer (AbstractLogger) to simplify testing
- ✅ Verify log level rather than full message
- ✅ Use CompositeLogger to combine Mock with actual output
- ✅ Ensure sensitive data is not logged
3. **Structured Output**
- ✅ Use section headings to separate different phases
- ✅ Include timestamps for easy tracking
- ✅ Provide sufficient context information
## DON'T - Practices to Avoid
1. **Don't Overuse Output**
- ❌ Avoid heavy output in every test
- ❌ Don't log sensitive information (passwords, keys)
- ❌ Avoid affecting test execution performance
2. **Don't Hardcode Log Verification**
- ❌ Avoid verifying complete log messages (fragile)
- ❌ Don't verify exact number of log calls (overspecified)
- ❌ Avoid testing internal implementation details
3. **Don't Ignore Lifecycle**
- ❌ Don't use ITestOutputHelper in static methods
- ❌ Don't attempt to share instances across test methods
- ❌ Avoid missing awaits in async tests
## Example Reference
See `templates/` directory for complete examples:
- `itestoutputhelper-example.cs` - ITestOutputHelper usage example
- `ilogger-testing-example.cs` - ILogger testing strategy example
- `diagnostic-tools.cs` - XUnitLogger and CompositeLogger implementation
## Reference Resources
### Original Articles
This skill content is distilled from the "Old School Software Engineer's Testing Practice - 30 Day Challenge" article series:
- **Day 08 - Test Output and Logging: xUnit ITestOutputHelper and ILogger**
- Article: https://ithelp.ithome.com.tw/articles/10374711
- Sample Code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day08
### Official Documentation
- [xUnit Capturing Output](https://xunit.net/docs/capturing-output)
### Related Skills
- `unit-test-fundamentals` - Unit testing basics
- `xunit-project-setup` - xUnit project setup
- `nsubstitute-mocking` - Test doubles and mocking
## Testing Checklist
When implementing test output and logging, confirm the following checklist items:
- [ ] ITestOutputHelper correctly injected via constructor
- [ ] Using structured output format (sections, timestamps)
- [ ] Logger tests use abstraction layer or CompositeLogger
- [ ] Verifying log level rather than full message
- [ ] Performance tests include timing point recording
- [ ] No sensitive information leaked in output
- [ ] Async tests properly await log completion
- [ ] Sufficient diagnostic information when tests fail
## Reference Resources (continued)
See example files in same directory:
- [templates/itestoutputhelper-example.cs](templates/itestoutputhelper-example.cs) - ITestOutputHelper usage example
- [templates/ilogger-testing-example.cs](templates/ilogger-testing-example.cs) - ILogger testing example
- [templates/diagnostic-tools.cs](templates/diagnostic-tools.cs) - Diagnostic tool implementation
1---2name: dotnet-testing-test-output-logging3category: testing4subcategory: fundamentals5description: |6 Complete guide for xUnit test output and logging. Use when you need to implement test output, diagnostic logging, or ILogger alternatives in xUnit tests. Covers ITestOutputHelper injection, AbstractLogger pattern, structured output design. Includes XUnitLogger, CompositeLogger, performance test diagnostic tool implementations.7 Keywords: ITestOutputHelper, ILogger testing, test output xunit, test output, test logging, AbstractLogger, XUnitLogger, CompositeLogger, testOutputHelper.WriteLine, test diagnostics, logger mock, test log, structured output, Received().Log8targets: ['*']9license: MIT10metadata:11 author: Kevin Tseng12 version: '1.0.0'13 tags: 'xunit, ITestOutputHelper, ILogger, testing, diagnostics, logging'14 related_skills: 'unit-test-fundamentals, nsubstitute-mocking, xunit-project-setup'15claudecode: {}16opencode: {}17codexcli:18 short-description: '.NET skill guidance for dotnet-testing-test-output-logging'19copilot: {}20geminicli: {}21antigravity: {}22---2324Source: kevintsengtw/dotnet-testing-agent-skills (MIT). Ported into dotnet-agent-harness.2526# Test Output and Logging Expert Guide2728This skill helps you implement high-quality test output and logging mechanisms in .NET xUnit test projects.2930## Applicable Scenarios3132Use this skill when asked to perform the following tasks:3334- Use ITestOutputHelper in xUnit tests to output diagnostic information35- Implement ILogger test alternatives (XUnitLogger)36- Create AbstractLogger or CompositeLogger patterns37- Design structured test output for debugging38- Implement performance test diagnostic tools3940## Core Principles4142### 1. ITestOutputHelper Usage Principles4344### Correct Injection Method4546- Inject `ITestOutputHelper` via constructor47- Each test class instance bound to test method48- Cannot be shared between static methods or across test methods4950````csharp51public class MyTests52{53 private readonly ITestOutputHelper _output;5455 public MyTests(ITestOutputHelper testOutputHelper)56 {57 _output = testOutputHelper;58 }59}60```text6162### Common Mistakes6364- ❌ Static access: `private static ITestOutputHelper _output`65- ❌ Using without awaiting in async tests66- ❌ Attempting to use in Dispose method6768### 2. Structured Output Format Design6970### Recommended Output Structure7172```csharp73private void LogSection(string title)74{75 _output.WriteLine($"\n=== {title} ===");76}7778private void LogKeyValue(string key, object value)79{80 _output.WriteLine($"{key}: {value}");81}8283private void LogTimestamp(DateTime time)84{85 _output.WriteLine($"Execution Time: {time:yyyy-MM-dd HH:mm:ss.fff}");86}87```text8889### Output Timing9091- At test start: Log test setup and input data92- During execution: Log important state changes93- Before assertion: Log expected and actual values94- At test end: Log execution time and result summary9596### 3. ILogger Testing Strategy9798### Challenge: Extension Methods Cannot Be Directly Mocked99100`ILogger.LogError()` is an extension method, NSubstitute cannot directly intercept. Need to intercept underlying `Log<TState>` method:101102```csharp103// ❌ Wrong: Directly mocking extension method fails104logger.Received().LogError(Arg.Any<string>());105106// ✅ Correct: Intercept underlying method107logger.Received().Log(108 LogLevel.Error,109 Arg.Any<EventId>(),110 Arg.Is<object>(o => o.ToString().Contains("expected message")),111 Arg.Any<Exception>(),112 Arg.Any<Func<object, Exception, string>>()113);114```text115116### Solution: Use Abstraction Layer117118Create `AbstractLogger<T>` to simplify testing:119120```csharp121public abstract class AbstractLogger<T> : ILogger<T>122{123 public IDisposable BeginScope<TState>(TState state)124 => null;125126 public bool IsEnabled(LogLevel logLevel)127 => true;128129 public void Log<TState>(130 LogLevel logLevel,131 EventId eventId,132 TState state,133 Exception exception,134 Func<TState, Exception, string> formatter)135 {136 Log(logLevel, exception, state?.ToString() ?? string.Empty);137 }138139 public abstract void Log(LogLevel logLevel, Exception ex, string information);140}141```text142143### Using in Tests144145```csharp146var logger = Substitute.For<AbstractLogger<MyService>>();147// Now can simply verify148logger.Received().Log(LogLevel.Error, Arg.Any<Exception>(), Arg.Is<string>(s => s.Contains("error message")));149```text150151### 4. Diagnostic Tool Integration152153### XUnitLogger: Direct Logs to Test Output154155```csharp156public class XUnitLogger<T> : ILogger<T>157{158 private readonly ITestOutputHelper _testOutputHelper;159160 public XUnitLogger(ITestOutputHelper testOutputHelper)161 {162 _testOutputHelper = testOutputHelper;163 }164165 public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,166 Exception exception, Func<TState, Exception, string> formatter)167 {168 var message = formatter(state, exception);169 _testOutputHelper.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] [{logLevel}] [{typeof(T).Name}] {message}");170 if (exception != null)171 {172 _testOutputHelper.WriteLine($"Exception: {exception}");173 }174 }175176 // Other necessary interface implementations...177}178```text179180### CompositeLogger: Support Both Verification and Output181182```csharp183public class CompositeLogger<T> : ILogger<T>184{185 private readonly ILogger<T>[] _loggers;186187 public CompositeLogger(params ILogger<T>[] loggers)188 {189 _loggers = loggers;190 }191192 public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,193 Exception exception, Func<TState, Exception, string> formatter)194 {195 foreach (var logger in _loggers)196 {197 logger.Log(logLevel, eventId, state, exception, formatter);198 }199 }200201 // Other interface implementations delegate to all internal loggers...202}203```text204205### Usage206207```csharp208// Perform both behavior verification and test output209var mockLogger = Substitute.For<AbstractLogger<MyService>>();210var xunitLogger = new XUnitLogger<MyService>(_output);211var compositeLogger = new CompositeLogger<MyService>(mockLogger, xunitLogger);212213var service = new MyService(compositeLogger);214```text215216## Implementation Guide217218### Timing Recording in Performance Tests219220```csharp221[Fact]222public async Task ProcessLargeDataSet_Performance_Test()223{224 // Arrange225 var stopwatch = Stopwatch.StartNew();226 var checkpoints = new List<(string Stage, TimeSpan Elapsed)>();227228 _output.WriteLine("Starting large dataset processing...");229230 // Act & Monitor231 await processor.LoadData(dataSet);232 checkpoints.Add(("Data Load", stopwatch.Elapsed));233 _output.WriteLine($"Data load complete: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");234235 await processor.ProcessData();236 checkpoints.Add(("Data Processing", stopwatch.Elapsed));237 _output.WriteLine($"Data processing complete: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");238239 stopwatch.Stop();240241 // Assert & Report242 _output.WriteLine("\n=== Performance Report ===");243 foreach (var (stage, elapsed) in checkpoints)244 {245 _output.WriteLine($"{stage}: {elapsed.TotalMilliseconds:F2} ms");246 }247}248```text249250### Diagnostic Test Base Class251252```csharp253public abstract class DiagnosticTestBase254{255 protected readonly ITestOutputHelper Output;256257 protected DiagnosticTestBase(ITestOutputHelper output)258 {259 Output = output;260 }261262 protected void LogTestStart(string testName)263 {264 Output.WriteLine($"\n=== {testName} ===");265 Output.WriteLine($"Execution Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");266 }267268 protected void LogTestData(object data)269 {270 Output.WriteLine($"Test Data: {JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true })}");271 }272273 protected void LogAssertionFailure(string field, object expected, object actual)274 {275 Output.WriteLine("\n=== Assertion Failure ===");276 Output.WriteLine($"Field: {field}");277 Output.WriteLine($"Expected: {expected}");278 Output.WriteLine($"Actual: {actual}");279 }280}281```text282283## DO - Recommended Practices2842851. **Appropriate Use of ITestOutputHelper**286 - ✅ Log important steps in complex tests287 - ✅ Adopt consistent structured output format288 - ✅ Provide diagnostic information when tests fail289 - ✅ Record timing points in performance tests2902912. **Logger Testing Strategy**292 - ✅ Use abstraction layer (AbstractLogger) to simplify testing293 - ✅ Verify log level rather than full message294 - ✅ Use CompositeLogger to combine Mock with actual output295 - ✅ Ensure sensitive data is not logged2962973. **Structured Output**298 - ✅ Use section headings to separate different phases299 - ✅ Include timestamps for easy tracking300 - ✅ Provide sufficient context information301302## DON'T - Practices to Avoid3033041. **Don't Overuse Output**305 - ❌ Avoid heavy output in every test306 - ❌ Don't log sensitive information (passwords, keys)307 - ❌ Avoid affecting test execution performance3083092. **Don't Hardcode Log Verification**310 - ❌ Avoid verifying complete log messages (fragile)311 - ❌ Don't verify exact number of log calls (overspecified)312 - ❌ Avoid testing internal implementation details3133143. **Don't Ignore Lifecycle**315 - ❌ Don't use ITestOutputHelper in static methods316 - ❌ Don't attempt to share instances across test methods317 - ❌ Avoid missing awaits in async tests318319## Example Reference320321See `templates/` directory for complete examples:322323- `itestoutputhelper-example.cs` - ITestOutputHelper usage example324- `ilogger-testing-example.cs` - ILogger testing strategy example325- `diagnostic-tools.cs` - XUnitLogger and CompositeLogger implementation326327## Reference Resources328329### Original Articles330331This skill content is distilled from the "Old School Software Engineer's Testing Practice - 30 Day Challenge" article series:332333- **Day 08 - Test Output and Logging: xUnit ITestOutputHelper and ILogger**334 - Article: https://ithelp.ithome.com.tw/articles/10374711335 - Sample Code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day08336337### Official Documentation338339- [xUnit Capturing Output](https://xunit.net/docs/capturing-output)340341### Related Skills342343- `unit-test-fundamentals` - Unit testing basics344- `xunit-project-setup` - xUnit project setup345- `nsubstitute-mocking` - Test doubles and mocking346347## Testing Checklist348349When implementing test output and logging, confirm the following checklist items:350351- [ ] ITestOutputHelper correctly injected via constructor352- [ ] Using structured output format (sections, timestamps)353- [ ] Logger tests use abstraction layer or CompositeLogger354- [ ] Verifying log level rather than full message355- [ ] Performance tests include timing point recording356- [ ] No sensitive information leaked in output357- [ ] Async tests properly await log completion358- [ ] Sufficient diagnostic information when tests fail359360## Reference Resources (continued)361362See example files in same directory:363364- [templates/itestoutputhelper-example.cs](templates/itestoutputhelper-example.cs) - ITestOutputHelper usage example365- [templates/ilogger-testing-example.cs](templates/ilogger-testing-example.cs) - ILogger testing example366- [templates/diagnostic-tools.cs](templates/diagnostic-tools.cs) - Diagnostic tool implementation367````