Skip to content

TUXU0001: Assert.Throws conversion loses ability to access exception properties (.Message, custom properties) #4419

Description

@thomhurst

Description

When the xUnit migration code fixer converts Assert.Throws<TException>(), subsequent access to the exception's properties (like .Message or custom properties) no longer compiles because the return type changes from the exception type to ThrowsAssertion<TException>.

Steps to Reproduce

Before (xUnit):

[Fact]
public void ThrowsWithMessage()
{
    var mock = new Mock<IFoo>();
    mock.Setup(x => x.Submit()).Verifiable();

    var mex = Assert.Throws<MockException>(() => mock.Verify());
    Assert.True(mex.IsVerificationError);  // Access custom property
    Assert.Contains("Kaboom!", mex.Message);  // Access Message property
}

After code fixer (invalid):

[Test]
public async Task ThrowsWithMessage()
{
    var mock = new Mock<IFoo>();
    mock.Setup(x => x.Submit()).Verifiable();

    var mex = Assert.Throws<MockException>(() => mock.Verify());
    Assert.True(mex.IsVerificationError);  // CS1061: 'ThrowsAssertion<MockException>' doesn't have IsVerificationError
    Assert.Contains("Kaboom!", mex.Message);  // CS1061: 'ThrowsAssertion<MockException>' doesn't have Message
}

Compilation Errors:

error CS1061: 'ThrowsAssertion<MockException>' does not contain a definition for 'IsVerificationError' 
and no accessible extension method 'IsVerificationError' accepting a first argument of type 
'ThrowsAssertion<MockException>' could be found

error CS1061: 'ThrowsAssertion<MockException>' does not contain a definition for 'Message' 
and no accessible extension method 'Message' accepting a first argument of type 
'ThrowsAssertion<MockException>' could be found

Expected Behavior

The code fixer should properly convert patterns where the exception is captured and its properties are accessed.

In TUnit, this should be:

[Test]
public async Task ThrowsWithMessage()
{
    var mock = new Mock<IFoo>();
    mock.Setup(x => x.Submit()).Verifiable();

    var mex = await Assert.ThrowsAsync<MockException>(() => mock.Verify());
    await Assert.That(mex.IsVerificationError).IsTrue();
    await Assert.That(mex.Message).Contains("Kaboom!");
}

Or using TUnit's fluent assertion pattern:

await Assert.That(() => mock.Verify())
    .ThrowsException<MockException>()
    .WithMessage(m => m.Contains("Kaboom!"));

Actual Behavior

The code fixer:

  1. Keeps the Assert.Throws<T>() call unchanged (doesn't convert to Assert.ThrowsAsync<T>())
  2. Creates an ambiguous reference because Xunit.Assert and TUnit.Assertions.Assert both exist
  3. Doesn't convert the subsequent assertions that access exception properties

Common Patterns Affected

This issue affects all tests that:

  1. Capture the thrown exception: var ex = Assert.Throws<T>(() => ...)
  2. Then assert on exception properties:
    • Assert.Equal("message", ex.Message)
    • Assert.Contains("text", ex.Message)
    • Assert.True(ex.SomeProperty)
    • Any custom exception property access

Environment

  • TUnit Version: 1.11.*
  • .NET SDK: 10.0.102
  • OS: Windows

Suggested Fix

The code fixer should:

  1. Detect when Assert.Throws<T>() result is assigned to a variable
  2. Convert the pattern to TUnit's equivalent that returns the exception:
    var ex = await Assert.ThrowsAsync<T>(async () => ...);
  3. Convert subsequent assertions on exception properties to TUnit assertions:
    await Assert.That(ex.Message).Contains("expected text");
    await Assert.That(ex.CustomProperty).IsTrue();

Alternatively, if using the fluent pattern:

await Assert.That(action)
    .ThrowsException<T>()
    .And.HasMessage(m => m.Contains("text"));

Tests Needed

Please add tests to verify:

  1. var ex = Assert.Throws<T>(...) followed by ex.Message access converts correctly
  2. var ex = Assert.Throws<T>(...) followed by custom property access converts correctly
  3. Multiple assertions on the same exception variable convert correctly
  4. Nested assertions on exception properties work
  5. The converted code compiles and executes correctly

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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