dotnet test produces results, but everyone who wants to see them has to go somewhere different: the Azure DevOps pipeline tab, a CI artifact zip, or the Visual Studio Test Explorer on whoever ran them locally. None of those work for a quick “did the suite pass?” link in a PR or Slack thread. There’s a simpler path that reuses output your build already generates.
The .NET Test Reporting Problem
dotnet test runs MSTest, NUnit, and xUnit suites and prints results to the console. Turning that console output into something a teammate can open in a browser is left to you, and the common options each trap the results somewhere.
What Teams Do Instead
1. Azure DevOps “Publish Test Results” task
- Solid in-pipeline view: pass/fail counts, history, failed-test drill-down
- Locked to the Azure DevOps pipeline UI. There’s no link you can hand to someone outside the org
- Tied to the build it ran in. No unified view if some suites run elsewhere or in a different pipeline
2. Visual Studio Test Explorer
- Good for the developer at the keyboard
- Local only. There’s nothing to share. Results live and die in one IDE session
3. CI artifacts (.trx zips)
- The
.trxfile uploads as a build artifact - Recipients need CI access to download and a TRX viewer to read it
- Artifacts expire (90 days max on GitHub Actions) and carry no historical trends
4. Pasting console output into Slack
- Loses the structure: no per-test status, no stack traces you can expand
- Truncated for large suites
- No way to filter failures or compare against the last run
Better: TRX + Hosted Reports
You don’t need a new reporter. dotnet test --logger:trx writes a .trx file, and it does so whether the project uses MSTest, NUnit, or xUnit. The dotnet test CLI emits TRX for all three. Gaffer parses TRX natively, so the integration is: tell dotnet test to write TRX, then upload the file in CI.
The setup:
- Run
dotnet test --logger:trx - Upload the
.trxfile to Gaffer in CI - Get a shareable URL, analytics, and flaky test detection
If you prefer JUnit XML (for example to standardize across non-.NET suites), the JUnitXml.TestLogger NuGet package adds a --logger:"junit" option, and Gaffer parses JUnit XML natively too.
What This Looks Like
After CI runs:
- Anyone with the link sees the full hosted report in their browser
- Failures keep their structure: per-test status, duration, and stack traces
- Analytics accumulate over time: pass rates, flaky tests, slow tests
- No Azure DevOps access required to view results
Setting Up .NET Reporting with Gaffer
Step 1: Write a TRX File
Add the TRX logger to your test run. This works for MSTest, NUnit, and xUnit projects without changing test code:
dotnet test --logger:trx --results-directory ./TestResultsThis writes a .trx file into ./TestResults. The filename includes a timestamp, so reference the directory in the upload step rather than a fixed name.
Step 2: Add the Upload Step to CI
GitHub Actions:
- name: Run tests run: dotnet test --logger:trx --results-directory ./TestResults
- name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./TestResults/*.trxAzure Pipelines (curl):
- script: dotnet test --logger:trx --results-directory $(Build.SourcesDirectory)/TestResults displayName: Run tests
- script: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $(GAFFER_PROJECT_TOKEN)" \ -F "files=@$(ls TestResults/*.trx | head -n1)" \ -F 'tags={"commitSha":"$(Build.SourceVersion)","branch":"$(Build.SourceBranchName)"}' displayName: Upload to Gaffer condition: always()The if: always() and condition: always() guards matter: you want the report uploaded even when tests fail, since that’s when the report is most useful.
Step 3: Share the Link
After upload, drop the report URL into a PR, Slack, or your Azure DevOps work item. Recipients see structured results in their browser with no CI access needed.
Need to share with someone outside your organization, like a contractor or client? Generate a public share link with an expiration you control (1 hour to 30 days, or never). They view the results without a Gaffer account.
Comparing .NET Reporting Options
| Feature | Azure DevOps Test Results | CI Artifacts | Gaffer |
|---|---|---|---|
| Hosted reports | In pipeline UI | Download zips | Yes |
| Shareable links outside the org | No | With CI access | Yes |
| Analytics/trends | In pipeline | No | Yes |
| Flaky test detection | Limited | No | Yes |
| Works across frameworks | Per pipeline | N/A | Yes |
| Pricing | Included with Azure DevOps | Free | Free tier available |
When Azure DevOps Test Results Makes Sense
If your whole team lives in Azure DevOps and every suite runs in the same pipeline, the built-in “Publish Test Results” task covers the in-pipeline case well and costs nothing extra. The friction shows up when you need to share a link outside the org or unify results from suites that run in different pipelines or providers.
When Gaffer Makes Sense
Gaffer fits when you want a link anyone can open, one dashboard across multiple frameworks and pipelines, or test analytics and flaky detection without per-seat pricing.
Beyond .NET: Multi-Framework Support
TRX and JUnit XML are not .NET-specific. The same upload step handles results from any framework Gaffer parses, so a project with a .NET backend, a JavaScript frontend, and a Python service can report all three to one dashboard:
- .NET (TRX via
dotnet test --logger:trx) - JavaScript / TypeScript (Jest, Vitest, Playwright)
- Python (pytest via JUnit XML)
All results land in the same project with unified hosted reports and shared analytics.
Get Started
Gaffer’s free tier includes 500 MB of storage with 7-day retention. Enough to wire dotnet test --logger:trx into CI and see your suite hosted with a shareable link.