PageSourceSearch

https://tunit.dev/assets/js/8df0f4cd.67ea154b.js

js tunit.dev collected 2026-10-02 07:53:54 UTC 21,038 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunktunit_docs_site||=[]).push([[7622],{42604(e,t,n){n.r(t),n.d(t,{assets:()=>o,contentTitle:()=>c,default:()=>d,frontMatter:()=>r,metadata:()=>s,toc:()=>l});const s=JSON.parse('{"id":"writing-tests/artifacts","title":"Test Artifacts","description":"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.","source":"@site/docs/writing-tests/artifacts.md","sourceDirName":"writing-tests","slug":"/writing-tests/artifacts","permalink":"/docs/writing-tests/artifacts","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{},"sidebar":"docs","previous":{"title":"Test Context","permalink":"/docs/writing-tests/test-context"},"next":{"title":"property-injection","permalink":"/docs/writing-tests/property-injection"}}');var i=n(74848),a=n(28453);const r={},c="Test Artifacts",o={},l=[{value:"Test-Level Artifacts",id:"test-level-artifacts",level:2},{value:"Writing Artifacts to the Results Directory",id:"writing-artifacts-to-the-results-directory",level:3},{value:"Basic Usage",id:"basic-usage",level:3},{value:"Attaching Screenshots on Failure",id:"attaching-screenshots-on-failure",level:3},{value:"Attaching Multiple Artifacts",id:"attaching-multiple-artifacts",level:3},{value:"Session-Level Artifacts",id:"session-level-artifacts",level:2},{value:"Basic Usage",id:"basic-usage-1",level:3},{value:"Configuration Files",id:"configuration-files",level:3},{value:"Performance Reports",id:"performance-reports",level:3},{value:"Artifact Class",id:"artifact-class",level:2},{value:"Best Practices",id:"best-practices",level:2},{value:"1. Clean Up Artifacts",id:"1-clean-up-artifacts",level:3},{value:"2. Organize Artifacts by Test",id:"2-organize-artifacts-by-test",level:3},{value:"3. Only Attach on Failure",id:"3-only-attach-on-failure",level:3},{value:"4. Use Descriptive Names",id:"4-use-descriptive-names",level:3},{value:"5. Verify Files Exist",id:"5-verify-files-exist",level:3},{value:"Common Use Cases",id:"common-use-cases",level:2},{value:"Browser Testing with Playwright",id:"browser-testing-with-playwright",level:3},{value:"API Testing",id:"api-testing",level:3},{value:"Database Testing",id:"database-testing",level:3},{value:"Integration with Test Runners",id:"integration-with-test-runners",level:2},{value:"See Also",id:"see-also",level:2}];function h(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.header,{children:(0,i.jsx)(t.h1,{id:"test-artifacts",children:"Test Artifacts"})}),"\n",(0,i.jsx)(t.p,{children:"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."}),"\n",(0,i.jsx)(t.p,{children:"TUnit supports attaching artifacts at two levels:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Test-level artifacts"}),": Attached to individual tests"]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Session-level artifacts"}),": Attached to the entire test session"]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"test-level-artifacts",children:"Test-Level Artifacts"}),"\n",(0,i.jsxs)(t.p,{children:["Attach files to individual tests using ",(0,i.jsx)(t.code,{children:"TestContext.Current.Output.AttachArtifact()"}),"."]}),"\n",(0,i.jsx)(t.h3,{id:"writing-artifacts-to-the-results-directory",children:"Writing Artifacts to the Results Directory"}),"\n",(0,i.jsxs)(t.p,{children:["Use ",(0,i.jsx)(t.code,{children:"TestContext.ResultsDirectory"})," to place generated files alongside reports and other\nartifacts. The property returns the absolute directory selected by Microsoft.Testing.Platform,\nincluding any ",(0,i.jsx)(t.code,{children:"--results-directory"})," override."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[Test]\npublic async Task CaptureLog()\n{\n    var artifactPath = Path.Combine(TestC
1ontext.ResultsDirectory, "application.log");\n\n    await File.WriteAllTextAsync(artifactPath, "Diagnostic information");\n    TestContext.Current!.Output.AttachArtifact(artifactPath);\n}\n'})}),"\n",(0,i.jsx)(t.h3,{id:"basic-usage",children:"Basic Usage"}),"\n",(0,i.jsx)(t.p,{children:"The simplest way to attach an artifact is by providing just the file path:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[Test]\npublic async Task MyIntegrationTest()\n{\n    // Perform your test logic\n    var result = await PerformOperation();\n\n    // Attach an artifact using the simple overload\n    TestContext.Current!.Output.AttachArtifact("path/to/logfile.log");\n\n    // Or with a custom display name and description\n    TestContext.Current!.Output.AttachArtifact(\n        "path/to/logfile.log",\n        displayName: "Application Logs",\n        description: "Logs captured during test execution"\n    );\n\n    await Assert.That(result).IsEqualTo(expected);\n}\n'})}),"\n",(0,i.jsxs)(t.p,{children:["For more control, you can create an ",(0,i.jsx)(t.code,{children:"Artifact"})," object directly:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[Test]\npublic async Task MyIntegrationTest()\n{\n    // Attach an artifact using the full Artifact object\n    TestContext.Current!.Output.AttachArtifact(new Artifact\n    {\n        File = new FileInfo("path/to/logfile.log"),\n        DisplayName = "Application Logs",\n        Description = "Logs captured during test execution"\n    });\n}\n'})}),"\n",(0,i.jsx)(t.h3,{id:"attaching-screenshots-on-failure",children:"Attaching Screenshots on Failure"}),"\n",(0,i.jsx)(t.p,{children:"A common pattern is to capture a screenshot when a test fails:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'public class MyTests\n{\n    [After(Test)]\n    public async Task TakeScreenshotOnFailure()\n    {\n        var testContext = TestContext.Current;\n        \n        if (testContext?.Execution.Result?.State == TestState.Failed)\n        {\n            // Capture screenshot\n            var screenshotPath = await CaptureScreenshot();\n            \n            testContext.Output.AttachArtifact(new Artifact\n            {\n                File = new FileInfo(screenshotPath),\n                DisplayName = "Failure Screenshot",\n                Description = $"Screenshot captured when test \'{testContext.Metadata.TestName}\' failed"\n            });\n        }\n    }\n    \n    private async Task<string> CaptureScreenshot()\n    {\n        // Your screenshot capture logic\n        var path = $"screenshots/test-{Guid.NewGuid()}.png";\n        // ... capture screenshot to path ...\n        return path;\n    }\n}\n'})}),"\n",(0,i.jsx)(t.h3,{id:"attaching-multiple-artifacts",children:"Attaching Multiple Artifacts"}),"\n",(0,i.jsx)(t.p,{children:"You can attach multiple artifacts to a single test:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[Test]\npublic async Task ComplexIntegrationTest()\n{\n    // Test logic that generates multiple outputs\n    var httpLog = await ExecuteHttpRequests();\n    var dbLog = await QueryDatabase();\n    var traceLog = await CollectTraces();\n    \n    // Attach all artifacts\n    TestContext.Current!.Output.AttachArtifact(new Artifact\n    {\n        File = new FileInfo(httpLog),\n        DisplayName = "HTTP Requests",\n        Description = "All HTTP requests and responses"\n    });\n    \n    TestContext.Current.Output.AttachArtifact(new Artifact\n    {\n        File = new FileInfo(dbLog),\n        DisplayName = "Database Queries",\n        Description = "All database queries executed"\n    });\n    \n    TestContext.Current.Output.AttachArtifact(new Artifact\n    {\n        File = new FileInfo(traceLog),\n        DisplayName = "Trace Logs",\n        Description = "Application trace logs"\n    });\n}\n'})}),"\n",(0,i.jsx)(t.h2,{id:"session-level-artifacts",children:"Session-Level Artifacts"}),"\n",(0,i.jsxs)(t.p,{children:["Attach files to the entire test session using ",(0,i.jsx)(t.code,{children:"TestSessionContext.Current.AddArtifact()"}),". This is useful for artifacts that span multiple tests or provide context for the entire test run."]}),"\n",(0,i.jsx)(t.h3,{id:"basic-usage-1",children:"Basic Usage"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[Before(TestSession)]\npublic static void SetupTestSession()\n{\n    // Start capturing session-wide logs\n    var sessionLogPath = "test-session-log.txt";\n    StartLogging(sessionLogPath);\n    \n    // This artifact is available to the entire test session\n    TestSessionContext.Current!.AddArtifact(new Artifact\n    {\n        File = new FileInfo(sessionLogPath),\n        DisplayName = "Test Session Log",\n        Description = "Log file for the entire test session"\
1n    });\n}\n'})}),"\n",(0,i.jsx)(t.h3,{id:"configuration-files",children:"Configuration Files"}),"\n",(0,i.jsx)(t.p,{children:"Attach configuration files to document the test environment:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[Before(TestSession)]\npublic static void DocumentTestEnvironment()\n{\n    // Attach environment configuration\n    TestSessionContext.Current!.AddArtifact(new Artifact\n    {\n        File = new FileInfo("appsettings.test.json"),\n        DisplayName = "Test Configuration",\n        Description = "Application configuration used for this test run"\n    });\n    \n    // Attach environment info\n    var envInfo = CollectEnvironmentInfo();\n    File.WriteAllText("environment-info.json", envInfo);\n    \n    TestSessionContext.Current.AddArtifact(new Artifact\n    {\n        File = new FileInfo("environment-info.json"),\n        DisplayName = "Environment Information",\n        Description = "System and runtime environment details"\n    });\n}\n'})}),"\n",(0,i.jsx)(t.h3,{id:"performance-reports",children:"Performance Reports"}),"\n",(0,i.jsx)(t.p,{children:"Generate and attach performance reports for the entire test session:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[After(TestSession)]\npublic static void GeneratePerformanceReport()\n{\n    // Generate performance report after all tests complete\n    var reportPath = "performance-report.html";\n    GenerateReport(reportPath);\n    \n    TestSessionContext.Current!.AddArtifact(new Artifact\n    {\n        File = new FileInfo(reportPath),\n        DisplayName = "Performance Report",\n        Description = "Performance metrics for all tests in this session"\n    });\n}\n'})}),"\n",(0,i.jsx)(t.h2,{id:"artifact-class",children:"Artifact Class"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"Artifact"})," class has the following properties:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:"public class Artifact\n{\n    public required FileInfo File { get; init; }          // The file to attach\n    public required string DisplayName { get; init; }     // Human-readable name\n    public string? Description { get; init; }             // Optional description\n}\n"})}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"File"}),": A ",(0,i.jsx)(t.code,{children:"FileInfo"})," object pointing to the file. The file must exist at the time of attachment."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"DisplayName"}),': A short, descriptive name for the artifact (e.g., "Screenshot", "Logs", "Configuration").']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Description"}),": An optional longer description providing more context about the artifact."]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"best-practices",children:"Best Practices"}),"\n",(0,i.jsx)(t.h3,{id:"1-clean-up-artifacts",children:"1. Clean Up Artifacts"}),"\n",(0,i.jsx)(t.p,{children:"Consider cleaning up temporary artifact files after test execution to avoid accumulating files:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[After(TestSession)]\npublic static void CleanupArtifacts()\n{\n    var artifactDir = "test-artifacts";\n    if (Directory.Exists(artifactDir))\n    {\n        Directory.Delete(artifactDir, recursive: true);\n    }\n}\n'})}),"\n",(0,i.jsx)(t.h3,{id:"2-organize-artifacts-by-test",children:"2. Organize Artifacts by Test"}),"\n",(0,i.jsx)(t.p,{children:"Create a unique directory for each test's artifacts:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[Before(Test)]\npublic void SetupTestArtifactDirectory()\n{\n    var testName = TestContext.Current!.Metadata.TestName;\n    var sanitizedName = string.Concat(testName.Split(Path.GetInvalidFileNameChars()));\n    var artifactDir = Path.Combine("test-artifacts", sanitizedName);\n    Directory.CreateDirectory(artifactDir);\n    \n    TestContext.Current.StateBag["ArtifactDir"] = artifactDir;\n}\n\n[Test]\npublic void MyTest()\n{\n    var artifactDir = TestContext.Current!.StateBag["ArtifactDir"] as string\n        ?? throw new InvalidOperationException("ArtifactDir was not initialized");
1\n    var logPath = Path.Combine(artifactDir, "test.log");\n    \n    // ... test logic ...\n    \n    TestContext.Current.Output.AttachArtifact(new Artifact\n    {\n        File = new FileInfo(logPath),\n        DisplayName = "Test Log"\n    });\n}\n'})}),"\n",(0,i.jsx)(t.h3,{id:"3-only-attach-on-failure",children:"3. Only Attach on Failure"}),"\n",(0,i.jsx)(t.p,{children:"For large artifacts (videos, extensive logs), consider only attaching them when tests fail:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[After(Test)]\npublic async Task ConditionalArtifactAttachment()\n{\n    var testContext = TestContext.Current\n        ?? throw new InvalidOperationException("No active test context.");\n    \n    if (testContext?.Execution.Result?.State is TestState.Failed or TestState.Timeout)\n    {\n        // Only attach expensive artifacts on failure\n        var videoPath = await StopRecording();\n        \n        testContext.Output.AttachArtifact(new Artifact\n        {\n            File = new FileInfo(videoPath),\n            DisplayName = "Test Recording",\n            Description = "Video recording of the failed test"\n        });\n    }\n}\n'})}),"\n",(0,i.jsx)(t.h3,{id:"4-use-descriptive-names",children:"4. Use Descriptive Names"}),"\n",(0,i.jsx)(t.p,{children:"Provide clear, descriptive names and descriptions for your artifacts:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'// \u274c Not descriptive\nTestContext.Current!.Output.AttachArtifact(new Artifact\n{\n    File = new FileInfo("log.txt"),\n    DisplayName = "Log"\n});\n\n// \u2705 Descriptive and helpful\nTestContext.Current!.Output.AttachArtifact(new Artifact\n{\n    File = new FileInfo("http-trace.log"),\n    DisplayName = "HTTP Request Trace",\n    Description = "Complete trace of all HTTP requests including headers and response bodies"\n});\n'})}),"\n",(0,i.jsx)(t.h3,{id:"5-verify-files-exist",children:"5. Verify Files Exist"}),"\n",(0,i.jsx)(t.p,{children:"Always ensure the file exists before attaching:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'var logPath = "path/to/logfile.log";\n\nif (File.Exists(logPath))\n{\n    TestContext.Current!.Output.AttachArtifact(new Artifact\n    {\n        File = new FileInfo(logPath),\n        DisplayName = "Application Log"\n    });\n}\nelse\n{\n    TestContext.Current!.Output.WriteLine($"Warning: Log file not found at {logPath}");\n}\n'})}),"\n",(0,i.jsx)(t.h2,{id:"common-use-cases",children:"Common Use Cases"}),"\n",(0,i.jsx)(t.h3,{id:"browser-testing-with-playwright",children:"Browser Testing with Playwright"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[After(Test)]\npublic async Task CapturePlaywrightArtifacts()\n{\n    var testContext = TestContext.Current\n        ?? throw new InvalidOperationException("No active test context.");\n    \n    if (testContext.Execution.Result?.State != TestState.Passed)\n    {\n        // Capture screenshot\n        var screenshotPath = $"artifacts/screenshot-{testContext.Id}.png";\n        await _page.ScreenshotAsync(new() { Path = screenshotPath });\n        \n        testContext.Output.AttachArtifact(new Artifact\n        {\n            File = new FileInfo(screenshotPath),\n            DisplayName = "Browser Screenshot"\n        });\n        \n        // Capture video if enabled\n        if (_page.Video is not null)\n        {\n            await _page.CloseAsync();\n            var videoPath = await _page.Video!.PathAsync();\n            \n            testContext.Output.AttachArtifact(new Artifact\n            {\n                File = new FileInfo(videoPath),\n                DisplayName = "Browser Recording"\n            });\n        }\n    }\n}\n'})}),"\n",(0,i.jsx)(t.h3,{id:"api-testing",children:"API Testing"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[Test]\npublic async Task ApiIntegrationTest()\n{\n    var requestLog = new StringBuilder();\n    var responseLog = new StringBuilder();\n    \n    // Make API calls while logging\n    var response = await _httpClient.GetAsync("/api/endpoint");\n    requestLog.AppendLine($"GET /api/endpoint");\n    responseLog.AppendLine(await response.Content.ReadAsStringAsync());\n    \n    // Save and attach logs\n    var requestPath = "api-request.txt";\n    var responsePath = "api-response.txt";\n    \n    await File.WriteAllTextAsync(requestPath, requestLog.ToString());\n    await File.WriteAllTextAsync(responsePath, responseLog.ToString());\n    \n    TestContext.Current!.Output.AttachArtifact(new Artifact\n    {\n        File = new FileInfo(requestPath),\n        DisplayName = "API Request"\n    });\n    \n    TestContext.Current.Output.AttachArtifact(new Artifact\n    {\n        File = new FileInfo(responsePath),\n        DisplayName = "API Response"\n    });\n}\n'})}),"\n",(0,i.jsx)(t.h3,{id:"database-testing",children:"Database Testing"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:'[Test]\npublic async Task DatabaseIntegrationTest()\n{\n    var queryLog = new List<string>();\n    \n    // Execute queries while logging\n    foreach (var query in _queries)\n    {\n        await _connection.ExecuteAsync(query);\n        queryLog.Add(query);\n    }
1\n    \n    // Save query log\n    var logPath = "database-queries.sql";\n    await File.WriteAllLinesAsync(logPath, queryLog);\n    \n    TestContext.Current!.Output.AttachArtifact(new Artifact\n    {\n        File = new FileInfo(logPath),\n        DisplayName = "Database Queries",\n        Description = "All SQL queries executed during test"\n    });\n}\n'})}),"\n",(0,i.jsx)(t.h2,{id:"integration-with-test-runners",children:"Integration with Test Runners"}),"\n",(0,i.jsx)(t.p,{children:"Artifacts attached using TUnit are automatically forwarded to the underlying Microsoft.Testing.Platform infrastructure, which makes them available to:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Test result files (TRX, etc.)"}),"\n",(0,i.jsx)(t.li,{children:"CI/CD systems (GitHub Actions, Azure DevOps, etc.)"}),"\n",(0,i.jsx)(t.li,{children:"Test explorers in IDEs (Visual Studio, Rider, VS Code)"}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"The exact behavior depends on your test runner configuration and CI/CD platform."}),"\n",(0,i.jsx)(t.h2,{id:"see-also",children:"See Also"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"/docs/writing-tests/test-context",children:"Test Context"})," - Overview of TestContext"]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"/docs/writing-tests/hooks",children:"Hooks"})," - Using Before/After hooks"]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"/docs/execution/ci-cd-reporting",children:"CI/CD Reporting"})," - Integrating with CI systems"]}),"\n"]})]})}function d(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},28453(e,t,n){n.d(t,{R:()=>r,x:()=>c});var s=n(96540);const i={},a=s.createContext(i);function r(e){const t=s.useContext(a);return s.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:r(e.components),s.createElement(a.Provider,{value:t},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.