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-logging3description: 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. 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().Log4license: MIT5---67Source: kevintsengtw/dotnet-testing-agent-skills (MIT). Ported into dotnet-agent-harness.89# Test Output and Logging Expert Guide1011This skill helps you implement high-quality test output and logging mechanisms in .NET xUnit test projects.1213## Applicable Scenarios1415Use this skill when asked to perform the following tasks:1617- Use ITestOutputHelper in xUnit tests to output diagnostic information18- Implement ILogger test alternatives (XUnitLogger)19- Create AbstractLogger or CompositeLogger patterns20- Design structured test output for debugging21- Implement performance test diagnostic tools2223## Core Principles2425### 1. ITestOutputHelper Usage Principles2627### Correct Injection Method2829- Inject `ITestOutputHelper` via constructor30- Each test class instance bound to test method31- Cannot be shared between static methods or across test methods3233````csharp34public class MyTests35{36 private readonly ITestOutputHelper _output;3738 public MyTests(ITestOutputHelper testOutputHelper)39 {40 _output = testOutputHelper;41 }42}43```text4445### Common Mistakes4647- ❌ Static access: `private static ITestOutputHelper _output`48- ❌ Using without awaiting in async tests49- ❌ Attempting to use in Dispose method5051### 2. Structured Output Format Design5253### Recommended Output Structure5455```csharp56private void LogSection(string title)57{58 _output.WriteLine($"\n=== {title} ===");59}6061private void LogKeyValue(string key, object value)62{63 _output.WriteLine($"{key}: {value}");64}6566private void LogTimestamp(DateTime time)67{68 _output.WriteLine($"Execution Time: {time:yyyy-MM-dd HH:mm:ss.fff}");69}70```text7172### Output Timing7374- At test start: Log test setup and input data75- During execution: Log important state changes76- Before assertion: Log expected and actual values77- At test end: Log execution time and result summary7879### 3. ILogger Testing Strategy8081### Challenge: Extension Methods Cannot Be Directly Mocked8283`ILogger.LogError()` is an extension method, NSubstitute cannot directly intercept. Need to intercept underlying `Log<TState>` method:8485```csharp86// ❌ Wrong: Directly mocking extension method fails87logger.Received().LogError(Arg.Any<string>());8889// ✅ Correct: Intercept underlying method90logger.Received().Log(91 LogLevel.Error,92 Arg.Any<EventId>(),93 Arg.Is<object>(o => o.ToString().Contains("expected message")),94 Arg.Any<Exception>(),95 Arg.Any<Func<object, Exception, string>>()96);97```text9899### Solution: Use Abstraction Layer100101Create `AbstractLogger<T>` to simplify testing:102103```csharp104public abstract class AbstractLogger<T> : ILogger<T>105{106 public IDisposable BeginScope<TState>(TState state)107 => null;108109 public bool IsEnabled(LogLevel logLevel)110 => true;111112 public void Log<TState>(113 LogLevel logLevel,114 EventId eventId,115 TState state,116 Exception exception,117 Func<TState, Exception, string> formatter)118 {119 Log(logLevel, exception, state?.ToString() ?? string.Empty);120 }121122 public abstract void Log(LogLevel logLevel, Exception ex, string information);123}124```text125126### Using in Tests127128```csharp129var logger = Substitute.For<AbstractLogger<MyService>>();130// Now can simply verify131logger.Received().Log(LogLevel.Error, Arg.Any<Exception>(), Arg.Is<string>(s => s.Contains("error message")));132```text133134### 4. Diagnostic Tool Integration135136### XUnitLogger: Direct Logs to Test Output137138```csharp139public class XUnitLogger<T> : ILogger<T>140{141 private readonly ITestOutputHelper _testOutputHelper;142143 public XUnitLogger(ITestOutputHelper testOutputHelper)144 {145 _testOutputHelper = testOutputHelper;146 }147148 public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,149 Exception exception, Func<TState, Exception, string> formatter)150 {151 var message = formatter(state, exception);152 _testOutputHelper.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] [{logLevel}] [{typeof(T).Name}] {message}");153 if (exception != null)154 {155 _testOutputHelper.WriteLine($"Exception: {exception}");156 }157 }158159 // Other necessary interface implementations...160}161```text162163### CompositeLogger: Support Both Verification and Output164165```csharp166public class CompositeLogger<T> : ILogger<T>167{168 private readonly ILogger<T>[] _loggers;169170 public CompositeLogger(params ILogger<T>[] loggers)171 {172 _loggers = loggers;173 }174175 public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,176 Exception exception, Func<TState, Exception, string> formatter)177 {178 foreach (var logger in _loggers)179 {180 logger.Log(logLevel, eventId, state, exception, formatter);181 }182 }183184 // Other interface implementations delegate to all internal loggers...185}186```text187188### Usage189190```csharp191// Perform both behavior verification and test output192var mockLogger = Substitute.For<AbstractLogger<MyService>>();193var xunitLogger = new XUnitLogger<MyService>(_output);194var compositeLogger = new CompositeLogger<MyService>(mockLogger, xunitLogger);195196var service = new MyService(compositeLogger);197```text198199## Implementation Guide200201### Timing Recording in Performance Tests202203```csharp204[Fact]205public async Task ProcessLargeDataSet_Performance_Test()206{207 // Arrange208 var stopwatch = Stopwatch.StartNew();209 var checkpoints = new List<(string Stage, TimeSpan Elapsed)>();210211 _output.WriteLine("Starting large dataset processing...");212213 // Act & Monitor214 await processor.LoadData(dataSet);215 checkpoints.Add(("Data Load", stopwatch.Elapsed));216 _output.WriteLine($"Data load complete: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");217218 await processor.ProcessData();219 checkpoints.Add(("Data Processing", stopwatch.Elapsed));220 _output.WriteLine($"Data processing complete: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");221222 stopwatch.Stop();223224 // Assert & Report225 _output.WriteLine("\n=== Performance Report ===");226 foreach (var (stage, elapsed) in checkpoints)227 {228 _output.WriteLine($"{stage}: {elapsed.TotalMilliseconds:F2} ms");229 }230}231```text232233### Diagnostic Test Base Class234235```csharp236public abstract class DiagnosticTestBase237{238 protected readonly ITestOutputHelper Output;239240 protected DiagnosticTestBase(ITestOutputHelper output)241 {242 Output = output;243 }244245 protected void LogTestStart(string testName)246 {247 Output.WriteLine($"\n=== {testName} ===");248 Output.WriteLine($"Execution Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");249 }250251 protected void LogTestData(object data)252 {253 Output.WriteLine($"Test Data: {JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true })}");254 }255256 protected void LogAssertionFailure(string field, object expected, object actual)257 {258 Output.WriteLine("\n=== Assertion Failure ===");259 Output.WriteLine($"Field: {field}");260 Output.WriteLine($"Expected: {expected}");261 Output.WriteLine($"Actual: {actual}");262 }263}264```text265266## DO - Recommended Practices2672681. **Appropriate Use of ITestOutputHelper**269 - ✅ Log important steps in complex tests270 - ✅ Adopt consistent structured output format271 - ✅ Provide diagnostic information when tests fail272 - ✅ Record timing points in performance tests2732742. **Logger Testing Strategy**275 - ✅ Use abstraction layer (AbstractLogger) to simplify testing276 - ✅ Verify log level rather than full message277 - ✅ Use CompositeLogger to combine Mock with actual output278 - ✅ Ensure sensitive data is not logged2792803. **Structured Output**281 - ✅ Use section headings to separate different phases282 - ✅ Include timestamps for easy tracking283 - ✅ Provide sufficient context information284285## DON'T - Practices to Avoid2862871. **Don't Overuse Output**288 - ❌ Avoid heavy output in every test289 - ❌ Don't log sensitive information (passwords, keys)290 - ❌ Avoid affecting test execution performance2912922. **Don't Hardcode Log Verification**293 - ❌ Avoid verifying complete log messages (fragile)294 - ❌ Don't verify exact number of log calls (overspecified)295 - ❌ Avoid testing internal implementation details2962973. **Don't Ignore Lifecycle**298 - ❌ Don't use ITestOutputHelper in static methods299 - ❌ Don't attempt to share instances across test methods300 - ❌ Avoid missing awaits in async tests301302## Example Reference303304See `templates/` directory for complete examples:305306- `itestoutputhelper-example.cs` - ITestOutputHelper usage example307- `ilogger-testing-example.cs` - ILogger testing strategy example308- `diagnostic-tools.cs` - XUnitLogger and CompositeLogger implementation309310## Reference Resources311312### Original Articles313314This skill content is distilled from the "Old School Software Engineer's Testing Practice - 30 Day Challenge" article series:315316- **Day 08 - Test Output and Logging: xUnit ITestOutputHelper and ILogger**317 - Article: https://ithelp.ithome.com.tw/articles/10374711318 - Sample Code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day08319320### Official Documentation321322- [xUnit Capturing Output](https://xunit.net/docs/capturing-output)323324### Related Skills325326- `unit-test-fundamentals` - Unit testing basics327- `xunit-project-setup` - xUnit project setup328- `nsubstitute-mocking` - Test doubles and mocking329330## Testing Checklist331332When implementing test output and logging, confirm the following checklist items:333334- [ ] ITestOutputHelper correctly injected via constructor335- [ ] Using structured output format (sections, timestamps)336- [ ] Logger tests use abstraction layer or CompositeLogger337- [ ] Verifying log level rather than full message338- [ ] Performance tests include timing point recording339- [ ] No sensitive information leaked in output340- [ ] Async tests properly await log completion341- [ ] Sufficient diagnostic information when tests fail342343## Reference Resources (continued)344345See example files in same directory:346347- [templates/itestoutputhelper-example.cs](templates/itestoutputhelper-example.cs) - ITestOutputHelper usage example348- [templates/ilogger-testing-example.cs](templates/ilogger-testing-example.cs) - ILogger testing example349- [templates/diagnostic-tools.cs](templates/diagnostic-tools.cs) - Diagnostic tool implementation350````