VOOZH about

URL: https://tunit.dev/docs/writing-tests/artifacts/

⇱ Test Artifacts | TUnit


Skip to main content

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 FileInfo object 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