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-logging-23description: "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().Log"4---5Source: kevintsengtw/dotnet-testing-agent-skills (MIT). Ported into dotnet-agent-harness.67# Test Output and Logging Expert Guide89This skill helps you implement high-quality test output and logging mechanisms in .NET xUnit test projects.1011## Applicable Scenarios1213Use this skill when asked to perform the following tasks:1415- Use ITestOutputHelper in xUnit tests to output diagnostic information16- Implement ILogger test alternatives (XUnitLogger)17- Create AbstractLogger or CompositeLogger patterns18- Design structured test output for debugging19- Implement performance test diagnostic tools2021## Core Principles2223### 1. ITestOutputHelper Usage Principles2425### Correct Injection Method2627- Inject `ITestOutputHelper` via constructor28- Each test class instance bound to test method29- Cannot be shared between static methods or across test methods3031````csharp32public class MyTests33{34 private readonly ITestOutputHelper _output;3536 public MyTests(ITestOutputHelper testOutputHelper)37 {38 _output = testOutputHelper;39 }40}41```text4243### Common Mistakes4445- ❌ Static access: `private static ITestOutputHelper _output`46- ❌ Using without awaiting in async tests47- ❌ Attempting to use in Dispose method4849### 2. Structured Output Format Design5051### Recommended Output Structure5253```csharp54private void LogSection(string title)55{56 _output.WriteLine($"\n=== {title} ===");57}5859private void LogKeyValue(string key, object value)60{61 _output.WriteLine($"{key}: {value}");62}6364private void LogTimestamp(DateTime time)65{66 _output.WriteLine($"Execution Time: {time:yyyy-MM-dd HH:mm:ss.fff}");67}68```text6970### Output Timing7172- At test start: Log test setup and input data73- During execution: Log important state changes74- Before assertion: Log expected and actual values75- At test end: Log execution time and result summary7677### 3. ILogger Testing Strategy7879### Challenge: Extension Methods Cannot Be Directly Mocked8081`ILogger.LogError()` is an extension method, NSubstitute cannot directly intercept. Need to intercept underlying `Log<TState>` method:8283```csharp84// ❌ Wrong: Directly mocking extension method fails85logger.Received().LogError(Arg.Any<string>());8687// ✅ Correct: Intercept underlying method88logger.Received().Log(89 LogLevel.Error,90 Arg.Any<EventId>(),91 Arg.Is<object>(o => o.ToString().Contains("expected message")),92 Arg.Any<Exception>(),93 Arg.Any<Func<object, Exception, string>>()94);95```text9697### Solution: Use Abstraction Layer9899Create `AbstractLogger<T>` to simplify testing:100101```csharp102public abstract class AbstractLogger<T> : ILogger<T>103{104 public IDisposable BeginScope<TState>(TState state)105 => null;106107 public bool IsEnabled(LogLevel logLevel)108 => true;109110 public void Log<TState>(111 LogLevel logLevel,112 EventId eventId,113 TState state,114 Exception exception,115 Func<TState, Exception, string> formatter)116 {117 Log(logLevel, exception, state?.ToString() ?? string.Empty);118 }119120 public abstract void Log(LogLevel logLevel, Exception ex, string information);121}122```text123124### Using in Tests125126```csharp127var logger = Substitute.For<AbstractLogger<MyService>>();128// Now can simply verify129logger.Received().Log(LogLevel.Error, Arg.Any<Exception>(), Arg.Is<string>(s => s.Contains("error message")));130```text131132### 4. Diagnostic Tool Integration133134### XUnitLogger: Direct Logs to Test Output135136```csharp137public class XUnitLogger<T> : ILogger<T>138{139 private readonly ITestOutputHelper _testOutputHelper;140141 public XUnitLogger(ITestOutputHelper testOutputHelper)142 {143 _testOutputHelper = testOutputHelper;144 }145146 public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,147 Exception exception, Func<TState, Exception, string> formatter)148 {149 var message = formatter(state, exception);150 _testOutputHelper.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] [{logLevel}] [{typeof(T).Name}] {message}");151 if (exception != null)152 {153 _testOutputHelper.WriteLine($"Exception: {exception}");154 }155 }156157 // Other necessary interface implementations...158}159```text160161### CompositeLogger: Support Both Verification and Output162163```csharp164public class CompositeLogger<T> : ILogger<T>165{166 private readonly ILogger<T>[] _loggers;167168 public CompositeLogger(params ILogger<T>[] loggers)169 {170 _loggers = loggers;171 }172173 public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,174 Exception exception, Func<TState, Exception, string> formatter)175 {176 foreach (var logger in _loggers)177 {178 logger.Log(logLevel, eventId, state, exception, formatter);179 }180 }181182 // Other interface implementations delegate to all internal loggers...183}184```text185186### Usage187188```csharp189// Perform both behavior verification and test output190var mockLogger = Substitute.For<AbstractLogger<MyService>>();191var xunitLogger = new XUnitLogger<MyService>(_output);192var compositeLogger = new CompositeLogger<MyService>(mockLogger, xunitLogger);193194var service = new MyService(compositeLogger);195```text196197## Implementation Guide198199### Timing Recording in Performance Tests200201```csharp202[Fact]203public async Task ProcessLargeDataSet_Performance_Test()204{205 // Arrange206 var stopwatch = Stopwatch.StartNew();207 var checkpoints = new List<(string Stage, TimeSpan Elapsed)>();208209 _output.WriteLine("Starting large dataset processing...");210211 // Act & Monitor212 await processor.LoadData(dataSet);213 checkpoints.Add(("Data Load", stopwatch.Elapsed));214 _output.WriteLine($"Data load complete: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");215216 await processor.ProcessData();217 checkpoints.Add(("Data Processing", stopwatch.Elapsed));218 _output.WriteLine($"Data processing complete: {stopwatch.Elapsed.TotalMilliseconds:F2} ms");219220 stopwatch.Stop();221222 // Assert & Report223 _output.WriteLine("\n=== Performance Report ===");224 foreach (var (stage, elapsed) in checkpoints)225 {226 _output.WriteLine($"{stage}: {elapsed.TotalMilliseconds:F2} ms");227 }228}229```text230231### Diagnostic Test Base Class232233```csharp234public abstract class DiagnosticTestBase235{236 protected readonly ITestOutputHelper Output;237238 protected DiagnosticTestBase(ITestOutputHelper output)239 {240 Output = output;241 }242243 protected void LogTestStart(string testName)244 {245 Output.WriteLine($"\n=== {testName} ===");246 Output.WriteLine($"Execution Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");247 }248249 protected void LogTestData(object data)250 {251 Output.WriteLine($"Test Data: {JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true })}");252 }253254 protected void LogAssertionFailure(string field, object expected, object actual)255 {256 Output.WriteLine("\n=== Assertion Failure ===");257 Output.WriteLine($"Field: {field}");258 Output.WriteLine($"Expected: {expected}");259 Output.WriteLine($"Actual: {actual}");260 }261}262```text263264## DO - Recommended Practices2652661. **Appropriate Use of ITestOutputHelper**267 - ✅ Log important steps in complex tests268 - ✅ Adopt consistent structured output format269 - ✅ Provide diagnostic information when tests fail270 - ✅ Record timing points in performance tests2712722. **Logger Testing Strategy**273 - ✅ Use abstraction layer (AbstractLogger) to simplify testing274 - ✅ Verify log level rather than full message275 - ✅ Use CompositeLogger to combine Mock with actual output276 - ✅ Ensure sensitive data is not logged2772783. **Structured Output**279 - ✅ Use section headings to separate different phases280 - ✅ Include timestamps for easy tracking281 - ✅ Provide sufficient context information282283## DON'T - Practices to Avoid2842851. **Don't Overuse Output**286 - ❌ Avoid heavy output in every test287 - ❌ Don't log sensitive information (passwords, keys)288 - ❌ Avoid affecting test execution performance2892902. **Don't Hardcode Log Verification**291 - ❌ Avoid verifying complete log messages (fragile)292 - ❌ Don't verify exact number of log calls (overspecified)293 - ❌ Avoid testing internal implementation details2942953. **Don't Ignore Lifecycle**296 - ❌ Don't use ITestOutputHelper in static methods297 - ❌ Don't attempt to share instances across test methods298 - ❌ Avoid missing awaits in async tests299300## Example Reference301302See `templates/` directory for complete examples:303304- `itestoutputhelper-example.cs` - ITestOutputHelper usage example305- `ilogger-testing-example.cs` - ILogger testing strategy example306- `diagnostic-tools.cs` - XUnitLogger and CompositeLogger implementation307308## Reference Resources309310### Original Articles311312This skill content is distilled from the "Old School Software Engineer's Testing Practice - 30 Day Challenge" article series:313314- **Day 08 - Test Output and Logging: xUnit ITestOutputHelper and ILogger**315 - Article: https://ithelp.ithome.com.tw/articles/10374711316 - Sample Code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day08317318### Official Documentation319320- [xUnit Capturing Output](https://xunit.net/docs/capturing-output)321322### Related Skills323324- `unit-test-fundamentals` - Unit testing basics325- `xunit-project-setup` - xUnit project setup326- `nsubstitute-mocking` - Test doubles and mocking327328## Testing Checklist329330When implementing test output and logging, confirm the following checklist items:331332- [ ] ITestOutputHelper correctly injected via constructor333- [ ] Using structured output format (sections, timestamps)334- [ ] Logger tests use abstraction layer or CompositeLogger335- [ ] Verifying log level rather than full message336- [ ] Performance tests include timing point recording337- [ ] No sensitive information leaked in output338- [ ] Async tests properly await log completion339- [ ] Sufficient diagnostic information when tests fail340341## Reference Resources (continued)342343See example files in same directory:344345- [templates/itestoutputhelper-example.cs](templates/itestoutputhelper-example.cs) - ITestOutputHelper usage example346- [templates/ilogger-testing-example.cs](templates/ilogger-testing-example.cs) - ILogger testing example347- [templates/diagnostic-tools.cs](templates/diagnostic-tools.cs) - Diagnostic tool implementation348````