Test artifacts are files (screenshots, logs, videos, JSON dumps, etc.) that you can attach to your tests. They are invaluable for debugging test failures, especially in integration tests and end-to-end tests.
TUnit supports attaching artifacts at two levels:
- Test-level artifacts: Attached to individual tests
- Session-level artifacts: Attached to the entire test session
Test-Level Artifacts
Attach files to individual tests using TestContext.Current.Output.AttachArtifact().
Basic Usage
The simplest way to attach an artifact is by providing just the file path:
[Test]
publicasyncTaskMyIntegrationTest()
{
// Perform your test logic
var result =awaitPerformOperation();
// Attach an artifact using the simple overload
TestContext.Current!.Output.AttachArtifact("path/to/logfile.log");
// Or with a custom display name and description
TestContext.Current!.Output.AttachArtifact(
"path/to/logfile.log",
displayName:"Application Logs",
description:"Logs captured during test execution"
);
await Assert.That(result).IsEqualTo(expected);
}
For more control, you can create an Artifact object directly:
[Test]
publicasyncTaskMyIntegrationTest()
{
// Attach an artifact using the full Artifact object
TestContext.Current!.Output.AttachArtifact(newArtifact
{
File =newFileInfo("path/to/logfile.log"),
DisplayName ="Application Logs",
Description ="Logs captured during test execution"
});
}
Attaching Screenshots on Failure
A common pattern is to capture a screenshot when a test fails:
publicclassMyTests
{
[After(Test)]
publicasyncTaskTakeScreenshotOnFailure()
{
var testContext = TestContext.Current;
if(testContext?.Execution.Result?.State == TestState.Failed)
{
// Capture screenshot
var screenshotPath =awaitCaptureScreenshot();
testContext.Output.AttachArtifact(newArtifact
{
File =newFileInfo(screenshotPath),
DisplayName ="Failure Screenshot",
Description =$"Screenshot captured when test '{testContext.Metadata.TestName}' failed"
});
}
}
privateasyncTask<string>CaptureScreenshot()
{
// Your screenshot capture logic
var path =$"screenshots/test-{Guid.NewGuid()}.png";
// ... capture screenshot to path ...
return path;
}
}
Attaching Multiple Artifacts
You can attach multiple artifacts to a single test:
[Test]
publicasyncTaskComplexIntegrationTest()
{
// Test logic that generates multiple outputs
var httpLog =awaitExecuteHttpRequests();
var dbLog =awaitQueryDatabase();
var traceLog =awaitCollectTraces();
// Attach all artifacts
TestContext.Current!.Output.AttachArtifact(newArtifact
{
File =newFileInfo(httpLog),
DisplayName ="HTTP Requests",
Description ="All HTTP requests and responses"
});
TestContext.Current.Output.AttachArtifact(newArtifact
{
File =newFileInfo(dbLog),
DisplayName ="Database Queries",
Description ="All database queries executed"
});
TestContext.Current.Output.AttachArtifact(newArtifact
{
File =newFileInfo(traceLog),
DisplayName ="Trace Logs",
Description ="Application trace logs"
});
}
Session-Level Artifacts
Attach files to the entire test session using TestSessionContext.Current.AddArtifact(). This is useful for artifacts that span multiple tests or provide context for the entire test run.
Basic Usage
[Before(TestSession)]
publicstaticvoidSetupTestSession()
{
// Start capturing session-wide logs
var sessionLogPath ="test-session-log.txt";
StartLogging(sessionLogPath);
// This artifact is available to the entire test session
TestSessionContext.Current!.AddArtifact(newArtifact
{
File =newFileInfo(sessionLogPath),
DisplayName ="Test Session Log",
Description ="Log file for the entire test session"
});
}
Configuration Files
Attach configuration files to document the test environment:
[Before(TestSession)]
publicstaticvoidDocumentTestEnvironment()
{
// Attach environment configuration
TestSessionContext.Current!.AddArtifact(newArtifact
{
File =newFileInfo("appsettings.test.json"),
DisplayName ="Test Configuration",
Description ="Application configuration used for this test run"
});
// Attach environment info
var envInfo =CollectEnvironmentInfo();
File.WriteAllText("environment-info.json", envInfo);
TestSessionContext.Current.AddArtifact(newArtifact
{
File =newFileInfo("environment-info.json"),
DisplayName ="Environment Information",
Description ="System and runtime environment details"
});
}
Performance Reports
Generate and attach performance reports for the entire test session:
[After(TestSession)]
publicstaticvoidGeneratePerformanceReport()
{
// Generate performance report after all tests complete
var reportPath ="performance-report.html";
GenerateReport(reportPath);
TestSessionContext.Current!.AddArtifact(newArtifact
{
File =newFileInfo(reportPath),
DisplayName ="Performance Report",
Description ="Performance metrics for all tests in this session"
});
}
Artifact Class
The Artifact class has the following properties:
publicclassArtifact
{
public required FileInfo File {get;init;}// The file to attach
public required string DisplayName {get;init;}// Human-readable name
publicstring? Description {get;init;}// Optional description
}
- File: A
FileInfoobject pointing to the file. The file must exist at the time of attachment. - DisplayName: A short, descriptive name for the artifact (e.g., "Screenshot", "Logs", "Configuration").
- Description: An optional longer description providing more context about the artifact.
Best Practices
1. Clean Up Artifacts
Consider cleaning up temporary artifact files after test execution to avoid accumulating files:
[After(TestSession)]
publicstaticvoidCleanupArtifacts()
{
var artifactDir ="test-artifacts";
if(Directory.Exists(artifactDir))
{
Directory.Delete(artifactDir,recursive:true);
}
}
2. Organize Artifacts by Test
Create a unique directory for each test's artifacts:
[Before(Test)]
publicvoidSetupTestArtifactDirectory()
{
var testName = TestContext.Current!.Metadata.TestName;
var sanitizedName =string.Concat(testName.Split(Path.GetInvalidFileNameChars()));
var artifactDir = Path.Combine("test-artifacts", sanitizedName);
Directory.CreateDirectory(artifactDir);
TestContext.Current.StateBag["ArtifactDir"]= artifactDir;
}
[Test]
publicvoidMyTest()
{
var artifactDir =(string)TestContext.Current!.StateBag["ArtifactDir"];
var logPath = Path.Combine(artifactDir,"test.log");
// ... test logic ...
TestContext.Current.Output.AttachArtifact(newArtifact
{
File =newFileInfo(logPath),
DisplayName ="Test Log"
});
}
3. Only Attach on Failure
For large artifacts (videos, extensive logs), consider only attaching them when tests fail:
[After(Test)]
publicasyncTaskConditionalArtifactAttachment()
{
var testContext = TestContext.Current;
if(testContext?.Execution.Result?.State isTestState.Failedor TestState.Timeout)
{
// Only attach expensive artifacts on failure
var videoPath =awaitStopRecording();
testContext.Output.AttachArtifact(newArtifact
{
File =newFileInfo(videoPath),
DisplayName ="Test Recording",
Description ="Video recording of the failed test"
});
}
}
4. Use Descriptive Names
Provide clear, descriptive names and descriptions for your artifacts:
// ❌ Not descriptive
TestContext.Current!.Output.AttachArtifact(newArtifact
{
File =newFileInfo("log.txt"),
DisplayName ="Log"
});
// ✅ Descriptive and helpful
TestContext.Current!.Output.AttachArtifact(newArtifact
{
File =newFileInfo("http-trace.log"),
DisplayName ="HTTP Request Trace",
Description ="Complete trace of all HTTP requests including headers and response bodies"
});
5. Verify Files Exist
Always ensure the file exists before attaching:
var logPath ="path/to/logfile.log";
if(File.Exists(logPath))
{
TestContext.Current!.Output.AttachArtifact(newArtifact
{
File =newFileInfo(logPath),
DisplayName ="Application Log"
});
}
else
{
TestContext.Current!.Output.WriteLine($"Warning: Log file not found at {logPath}");
}
Common Use Cases
Browser Testing with Playwright
[After(Test)]
publicasyncTaskCapturePlaywrightArtifacts()
{
var testContext = TestContext.Current;
if(testContext?.Execution.Result?.State != TestState.Passed)
{
// Capture screenshot
var screenshotPath =$"artifacts/screenshot-{testContext.Id}.png";
await _page.ScreenshotAsync(new(){ Path = screenshotPath });
testContext.Output.AttachArtifact(newArtifact
{
File =newFileInfo(screenshotPath),
DisplayName ="Browser Screenshot"
});
// Capture video if enabled
if(_browserContext.Options?.RecordVideo !=null)
{
await _page.CloseAsync();
var videoPath =await _page.Video!.PathAsync();
testContext.Output.AttachArtifact(newArtifact
{
File =newFileInfo(videoPath),
DisplayName ="Browser Recording"
});
}
}
}
API Testing
[Test]
publicasyncTaskApiIntegrationTest()
{
var requestLog =newStringBuilder();
var responseLog =newStringBuilder();
// Make API calls while logging
var response =await _httpClient.GetAsync("/api/endpoint");
requestLog.AppendLine($"GET /api/endpoint");
responseLog.AppendLine(await response.Content.ReadAsStringAsync());
// Save and attach logs
var requestPath ="api-request.txt";
var responsePath ="api-response.txt";
await File.WriteAllTextAsync(requestPath, requestLog.ToString());
await File.WriteAllTextAsync(responsePath, responseLog.ToString());
TestContext.Current!.Output.AttachArtifact(newArtifact
{
File =newFileInfo(requestPath),
DisplayName ="API Request"
});
TestContext.Current.Output.AttachArtifact(newArtifact
{
File =newFileInfo(responsePath),
DisplayName ="API Response"
});
}
Database Testing
[Test]
publicasyncTaskDatabaseIntegrationTest()
{
var queryLog =newList<string>();
// Execute queries while logging
foreach(var query in _queries)
{
await _connection.ExecuteAsync(query);
queryLog.Add(query);
}
// Save query log
var logPath ="database-queries.sql";
await File.WriteAllLinesAsync(logPath, queryLog);
TestContext.Current!.Output.AttachArtifact(newArtifact
{
File =newFileInfo(logPath),
DisplayName ="Database Queries",
Description ="All SQL queries executed during test"
});
}
Integration with Test Runners
Artifacts attached using TUnit are automatically forwarded to the underlying Microsoft.Testing.Platform infrastructure, which makes them available to:
- Test result files (TRX, etc.)
- CI/CD systems (GitHub Actions, Azure DevOps, etc.)
- Test explorers in IDEs (Visual Studio, Rider, VS Code)
The exact behavior depends on your test runner configuration and CI/CD platform.
See Also
- Test Context - Overview of TestContext
- Hooks - Using Before/After hooks
- CI/CD Reporting - Integrating with CI systems
