Skip to content

[Bug]: 5 small papercuts on 1.36.0 found during MSTest→TUnit migration (assertion diagnostics, array equality, generic inference, analyzer ergonomics) #5613

@JohnVerheij

Description

@JohnVerheij

Description

Note: this issue batches 5 small findings from a TUnit migration (mix of bugs + ergonomic asks). Filing as one issue for context; happy to split into separate issues if preferred.

Building on #5525 (comment) — the migration is well underway (several hundred tests on TUnit 1.36.0, more landing per release). Overall the framework has been a pleasure — [Matrix], [ParallelLimiter<T>], [ClassDataSource<T>(SharedType.PerClass)] with required init, Assert.Multiple, and fluent Throws<T>() / ThrowsNothing() have all delivered. The TUnit 1.36 TUnit.OpenTelemetry + TestWebApplicationFactory work lines up cleanly for our upcoming HTTP-endpoint tests.

Along the way we hit five small rough edges. Each is verified against 1.36.0 with a minimal repro.

# Finding Severity
1 .IsNotNull() on Task produces a misleading compile error suggesting JsonElement Low — confusing diagnostic
2 IsEqualTo on arrays uses reference equality; failure message renders as "System.UInt32[] ≠ System.UInt32[]" Medium — silent footgun + unhelpful error
3 IsNotSameReferenceAs(task) fails to infer generic argument — #1573 handles object receivers; typed-reference receivers (e.g. Task) still fail Low
4 [MatrixDataSource] should be implicit when parameters carry [Matrix] (related to #4765's class-level cascade) Low — ergonomics
5 TUnit0015 has no Roslyn code-fix to auto-insert CancellationToken (#4767 demoted to warning; escalates to error under TreatWarningsAsErrors) Low — ergonomics

Expected Behavior

1. Assert.That(task).IsNotNull() should either (a) check the reference of the Task itself, or (b) fail to compile with a diagnostic that is actually relevant to Task — not one pointing at JsonElement.

2. Assert.That(array).IsEqualTo(array) should compare elements (matching xUnit, NUnit, FluentAssertions). When it fails, the error should format the contents ([2, 5, 7] vs [2, 5, 8]), not render both sides as "System.UInt32[]".

3. Assert.That(task).IsNotSameReferenceAs(otherTask) should infer the generic argument from the receiver, as #1572 / #1573 intended.

4. A test method with [Matrix] parameters should either (a) behave as if [MatrixDataSource] were applied implicitly, or (b) have a Roslyn code-fix that inserts [MatrixDataSource] on one keystroke when TUnit0049 fires.

5. TUnit0015 should ship with a code-fix that offers "Add CancellationToken parameter" (with variants for ThrowIfCancellationRequested, discard, or unused).

Actual Behavior

1. Compile error CS1929 points the user at JsonElementAssertionExtensions.IsNotNull(IAssertionSource<JsonElement>). Workaround: cast the Task to object first.

2. Assertion fails at runtime with the unhelpful message "Expected to be equal to System.UInt32[] but received System.UInt32[]". Arrays don't override ToString, so the diff is invisible. Workaround: element-by-element loop.

3. Compile error CS0411 — "type arguments cannot be inferred from the usage". #1573's fix works for object receivers; Task-typed receivers still trip CS0411. Workaround: explicit <Task> generic annotation.

4. Build error TUnit0049[MatrixDataSourceAttribute] is required if using [Matrix] values on your parameters. The user must add a second attribute manually; no code-fix.

5. TUnit0015 fires correctly but the fix is entirely manual: append the parameter, then either use it or discard it to silence IDE0060 / CA1801.

Steps to Reproduce

All 5 repros were verified in a fresh console project targeting TUnit 1.36.0 on .NET 10.0.202 (Windows).


1. Task.IsNotNull — misleading diagnostic:

[Test]
public async Task Issue1(CancellationToken ct)
{
    _ = ct;
    Task t = Task.CompletedTask;
    await Assert.That(t).IsNotNull();   // CS1929 — diagnostic suggests JsonElement
}

Workaround: await Assert.That((object)t).IsNotNull();


2. Array IsEqualTo — reference equality + uninformative failure:

[Test]
public async Task Issue2(CancellationToken ct)
{
    _ = ct;
    uint[] actual = [2u, 5u, 7u];
    uint[] expected = [2u, 5u, 7u];
    await Assert.That(actual).IsEqualTo(expected);   // fails: "System.UInt32[] ≠ System.UInt32[]"
}

Workaround: compare Length + iterate elementwise.


3. IsNotSameReferenceAs — generic inference fails:

[Test]
public async Task Issue3(CancellationToken ct)
{
    _ = ct;
    Task a = Task.CompletedTask;
    Task b = Task.FromResult(true);
    await Assert.That(a).IsNotSameReferenceAs(b);   // CS0411
}

Workaround: await Assert.That(a).IsNotSameReferenceAs<Task>(b);


4. [Matrix] without [MatrixDataSource]:

[Test]
public async Task Issue4(
    [Matrix(true, false)] bool flag,
    CancellationToken ct)
{
    _ = ct;
    await Assert.That(flag || !flag).IsTrue();   // TUnit0049 at build time
}

Workaround: add [MatrixDataSource] on the method.


5. TUnit0015 fires as warning — no code-fix, manual edit required:

[Test]
[Timeout(1000)]
public async Task Issue5()
{
    await Task.Yield();   // TUnit0015 — no code-fix; manual edit required
}

Workaround: add CancellationToken cancellationToken parameter, then _ = cancellationToken; or cancellationToken.ThrowIfCancellationRequested();.

TUnit Version

1.36.0

.NET Version

10.0.202

Operating System

Windows

IDE / Test Runner

dotnet CLI (dotnet test / dotnet run)

Error Output / Stack Trace

Additional Context

Suggested directions

1. Add IsNotNull / IsNull extension methods on AsyncDelegateAssertion that check the reference (not the delegate's awaited result), or ship an analyzer that catches the pattern and suggests the right form.

2. Either default IsEqualTo on T[] / IEnumerable<T> to SequenceEqual (matches xUnit, NUnit, FluentAssertions), or keep reference semantics but ship a dedicated IsSequenceEqualTo. Orthogonally: when both sides render as the same string, fall back to formatting the contents so the diff is visible.

3. The TValue parameter can be inferred from the receiver (IAssertionSource<TValue>). Changing the second-arg type from object? to TValue would let the compiler resolve without the annotation — matching how IsSameReferenceAs behaves for object receivers after #1573.

4. Two viable fixes: (a) treat [Matrix] on any parameter as implicit [MatrixDataSource] on the method, or (b) ship a Roslyn code-fix with TUnit0049 that inserts [MatrixDataSource] on one keystroke. Option (a) is cleaner long-term; option (b) is a faster shim. Adjacent to #4765 but scoped to per-method rather than class-level cascade.

5. Ship a code-fix with TUnit0015 offering three intents: Add CancellationToken parameter, Add CancellationToken parameter and ThrowIfCancellationRequested, Add CancellationToken parameter as discard. Meziantou.Analyzer ships code-fixes alongside most of its analyzers — similar pattern here would be welcome.

Configuration

No AOT / trimming involved. Standard TUnit NuGet package, default parallelism. None of the 5 findings are IDE-specific — all reproduced via dotnet run from the CLI in a fresh dotnet new console project.

Closing

Happy to test any fixes against the live migration — we re-run the full TUnit suite on every TUnit release, so reproduction of anything you fix is one dotnet test away on our side.

Thanks for the framework and the pace.

IDE-Specific Issue?

  • I've confirmed this issue occurs when running via dotnet test or dotnet run, not just in my IDE

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions