Skip to content

TRX: failing tests in global-namespace parameterized NUnit fixtures are silently dropped (build reports green) #10590

Description

Summary

When TRX reporting is enabled, ObjectModelConverters.ToTestNode throws InvalidOperationException for any NUnit test in a parameterized fixture declared in the global namespace. The exception is swallowed by the adapter's event handler, so the affected test results are silently dropped from both the TRX file and the run summary counts.

Critically, this includes failing tests: a failing test in such a fixture is discarded, failed: 0 is reported, and the process exits 0. A red build reports green.

Repro

Tests.cs:

using NUnit.Framework;

[TestFixture("1")]
public class GlobalFixture(string arg1)
{
    [Test]
    public void ThisTestFails() => Assert.Fail("I FAILED - this should turn the build red");
}

trxrepro.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <OutputType>Exe</OutputType>
    <EnableNUnitRunner>true</EnableNUnitRunner>
    <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
    <TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="NUnit" Version="4.6.1" />
    <PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
    <PackageReference Include="Microsoft.Testing.Extensions.TrxReport" Version="2.3.3" />
  </ItemGroup>
</Project>

Run:

dotnet run --report-trx

Actual

Error processing ThisTestFails event for GlobalFixture("1").ThisTestFails
System.InvalidOperationException: Unable to parse fully qualified type name from test case: GlobalFixture("1").ThisTestFails
   at Microsoft.Testing.Extensions.VSTestBridge.ObjectModel.ObjectModelConverters.ToTestNode(...) in /_/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.cs:line 157
   at Microsoft.Testing.Extensions.VSTestBridge.ObjectModel.FrameworkHandlerAdapter.RecordResult(TestResult testResult) in /_/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FrameworkHandlerAdapter.cs:line 136
   at NUnit.VisualStudio.TestAdapter.NUnitEventListener.TestFinished(...)
   at NUnit.VisualStudio.TestAdapter.NUnitEventListener.OnTestEvent(String report)

Test run summary: Zero tests ran
  total: 0
  failed: 0
  succeeded: 0
  skipped: 0

Process exit code: 0.

The failing test is reported nowhere. NUnit discovered and ran it (discovered 1 of 1), and it failed, but the result never reaches the summary or the TRX.

Expected

The run reports failed: 1 and exits non-zero. At minimum, an unparseable name on a reporting path should degrade gracefully rather than discard the result.

Two conditions are required

1. TRX must be enabled. Same project without --report-trx:

total: 2   (both tests counted)

with --report-trx:

total: 1   (global-namespace fixture's test silently dropped)

2. The fixture must be in the global namespace. Adding a namespace avoids it:

namespace Some.Namespace
{
    [TestFixture("1")]
    public class NamespacedFixture(string arg1)
    {
        [Test]
        public void Simple() => Assert.Pass(arg1);   // works fine
    }
}
  • Some.Namespace.NamespacedFixture("1").Simple — a . exists before the first (, parse succeeds.
  • GlobalFixture("1").Simple — first ( is at index 14 with no preceding ., parse fails.

Analysis

In ObjectModelConverters.ToTestNode, the throw sits inside the isTrxEnabled block, after the TestMethodIdentifierProperty and TryParseFullyQualifiedType fallbacks:

if (isTrxEnabled)
{
    if (testMethodIdentifierProperty is not null) { /* ... */ }
    else if (TryParseFullyQualifiedType(...)) { /* ... */ }
    else { throw new InvalidOperationException("Unable to parse fully qualified type name..."); }
}

TryParseFullyQualifiedType assumes a Namespace.Type.Method shape and looks for a . before the first (. NUnit emits parameterized-fixture names as Fixture("arg").Method, which has no such . when the fixture is in the global namespace.

Two things seem worth separating:

  1. The throw itself. This is a reporting-only concern with an existing non-throwing fallback path, so throwing — and having a caller swallow it, losing the result — seems disproportionate. Discarding a failed result and exiting 0 is the part that worries me most.
  2. The parse. [perf-improver] perf: eliminate eager testFullName allocation in MSTestTestNodeConverter #9823 proposes removing TryParseFullyQualifiedType in favour of using FullClassName directly. That's framed as a perf change, but it looks like it would also remove this failure mode. If that lands, this may resolve incidentally — though the swallow-and-drop behaviour in FrameworkHandlerAdapter.RecordResult would remain as a general hazard.

I appreciate the NUnit FQN format is awkward to consume (see nunit/nunit3-vs-adapter#404), and a TestMethodIdentifierProperty from the NUnit side would sidestep the parse entirely. Filing here because the silent loss of a failing result seems worth addressing on this side regardless of what NUnit emits.

Versions

Package Version
NUnit 4.6.1
NUnit3TestAdapter 6.2.0
Microsoft.NET.Test.Sdk 17.14.1
Microsoft.Testing.Extensions.TrxReport 2.3.3
TFM net8.0 (also repros on net11.0, net48)

Originally surfaced in VerifyTests/Verify CI, where ~23 test results across parameterized fixtures were dropped from the uploaded TRX without the build going red.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs/triageNeeds triage by a maintainer.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions