Skip to content

Commit 36c3108

Browse files
Copilotthomhurst
andcommitted
Fix NUnit ThrowsAsync migration with constraint losing test code
Co-authored-by: thomhurst <[email protected]>
1 parent 6016933 commit 36c3108

2 files changed

Lines changed: 327 additions & 21 deletions

File tree

TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs

Lines changed: 103 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -601,36 +601,118 @@ private ExpressionSyntax ConvertInstanceOf(SeparatedSyntaxList<ArgumentSyntax> a
601601

602602
private ExpressionSyntax ConvertNUnitThrows(InvocationExpressionSyntax invocation)
603603
{
604-
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
605-
memberAccess.Name is GenericNameSyntax genericName)
604+
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
606605
{
607-
var exceptionType = genericName.TypeArgumentList.Arguments[0];
608-
var action = invocation.ArgumentList.Arguments[0].Expression;
609-
610-
var throwsAsyncInvocation = SyntaxFactory.InvocationExpression(
611-
SyntaxFactory.MemberAccessExpression(
612-
SyntaxKind.SimpleMemberAccessExpression,
613-
SyntaxFactory.IdentifierName("Assert"),
614-
SyntaxFactory.GenericName("ThrowsAsync")
615-
.WithTypeArgumentList(
616-
SyntaxFactory.TypeArgumentList(
617-
SyntaxFactory.SingletonSeparatedList(exceptionType)
606+
// Handle generic form: Assert.Throws<T>(() => ...) or Assert.ThrowsAsync<T>(() => ...)
607+
if (memberAccess.Name is GenericNameSyntax genericName)
608+
{
609+
var exceptionType = genericName.TypeArgumentList.Arguments[0];
610+
var action = invocation.ArgumentList.Arguments[0].Expression;
611+
612+
var throwsAsyncInvocation = SyntaxFactory.InvocationExpression(
613+
SyntaxFactory.MemberAccessExpression(
614+
SyntaxKind.SimpleMemberAccessExpression,
615+
SyntaxFactory.IdentifierName("Assert"),
616+
SyntaxFactory.GenericName("ThrowsAsync")
617+
.WithTypeArgumentList(
618+
SyntaxFactory.TypeArgumentList(
619+
SyntaxFactory.SingletonSeparatedList(exceptionType)
620+
)
618621
)
622+
),
623+
SyntaxFactory.ArgumentList(
624+
SyntaxFactory.SingletonSeparatedList(
625+
SyntaxFactory.Argument(action)
619626
)
620-
),
621-
SyntaxFactory.ArgumentList(
622-
SyntaxFactory.SingletonSeparatedList(
623-
SyntaxFactory.Argument(action)
624627
)
625-
)
626-
);
628+
);
629+
630+
return SyntaxFactory.AwaitExpression(throwsAsyncInvocation);
631+
}
632+
633+
// Handle non-generic constraint-based form: Assert.Throws(constraint, () => ...) or Assert.ThrowsAsync(constraint, () => ...)
634+
// where constraint is typically Is.TypeOf(typeof(T))
635+
if (invocation.ArgumentList.Arguments.Count >= 2)
636+
{
637+
var constraint = invocation.ArgumentList.Arguments[0].Expression;
638+
var action = invocation.ArgumentList.Arguments[1].Expression;
639+
640+
// Try to extract the exception type from the constraint
641+
var exceptionType = TryExtractTypeFromConstraint(constraint);
642+
643+
if (exceptionType != null)
644+
{
645+
// Convert to generic ThrowsAsync form: Assert.ThrowsAsync<T>(() => ...)
646+
var throwsAsyncInvocation = SyntaxFactory.InvocationExpression(
647+
SyntaxFactory.MemberAccessExpression(
648+
SyntaxKind.SimpleMemberAccessExpression,
649+
SyntaxFactory.IdentifierName("Assert"),
650+
SyntaxFactory.GenericName("ThrowsAsync")
651+
.WithTypeArgumentList(
652+
SyntaxFactory.TypeArgumentList(
653+
SyntaxFactory.SingletonSeparatedList(exceptionType)
654+
)
655+
)
656+
),
657+
SyntaxFactory.ArgumentList(
658+
SyntaxFactory.SingletonSeparatedList(
659+
SyntaxFactory.Argument(action)
660+
)
661+
)
662+
);
627663

628-
return SyntaxFactory.AwaitExpression(throwsAsyncInvocation);
664+
return SyntaxFactory.AwaitExpression(throwsAsyncInvocation);
665+
}
666+
}
629667
}
630668

631-
// Fallback for non-generic Throws
669+
// Fallback for unsupported Throws patterns
632670
return CreateTUnitAssertion("Throws", invocation.ArgumentList.Arguments[0].Expression);
633671
}
672+
673+
/// <summary>
674+
/// Attempts to extract the exception type from NUnit constraint expressions like Is.TypeOf(typeof(T)).
675+
/// Returns null if the type cannot be extracted.
676+
/// </summary>
677+
private TypeSyntax? TryExtractTypeFromConstraint(ExpressionSyntax constraint)
678+
{
679+
// Handle Is.TypeOf(typeof(T)) pattern
680+
if (constraint is InvocationExpressionSyntax invocation)
681+
{
682+
// Check if it's a method call like Is.TypeOf(...) or TypeOf(...)
683+
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
684+
memberAccess.Name.Identifier.Text == "TypeOf" &&
685+
invocation.ArgumentList.Arguments.Count > 0)
686+
{
687+
// Extract the argument to TypeOf - should be typeof(T)
688+
var typeofArg = invocation.ArgumentList.Arguments[0].Expression;
689+
return ExtractTypeFromTypeof(typeofArg);
690+
}
691+
692+
// Handle standalone TypeOf(typeof(T)) calls
693+
if (invocation.Expression is IdentifierNameSyntax { Identifier.Text: "TypeOf" } &&
694+
invocation.ArgumentList.Arguments.Count > 0)
695+
{
696+
var typeofArg = invocation.ArgumentList.Arguments[0].Expression;
697+
return ExtractTypeFromTypeof(typeofArg);
698+
}
699+
}
700+
701+
return null;
702+
}
703+
704+
/// <summary>
705+
/// Extracts the type from a typeof(T) expression.
706+
/// </summary>
707+
private TypeSyntax? ExtractTypeFromTypeof(ExpressionSyntax expression)
708+
{
709+
if (expression is TypeOfExpressionSyntax typeofExpression)
710+
{
711+
return typeofExpression.Type;
712+
}
713+
714+
return null;
715+
}
634716

635717
private ExpressionSyntax CreatePassAssertion(SeparatedSyntaxList<ArgumentSyntax> arguments)
636718
{

TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1598,6 +1598,230 @@ public async Task MyTest(int value)
15981598
);
15991599
}
16001600

1601+
[Test]
1602+
public async Task NUnit_ThrowsAsync_Generic_Converted()
1603+
{
1604+
await CodeFixer.VerifyCodeFixAsync(
1605+
"""
1606+
using NUnit.Framework;
1607+
using System;
1608+
1609+
{|#0:public class MyClass|}
1610+
{
1611+
[Test]
1612+
public void TestMethod()
1613+
{
1614+
Assert.ThrowsAsync<ArgumentException>(async () => await SomeAsyncMethod());
1615+
}
1616+
1617+
private async System.Threading.Tasks.Task SomeAsyncMethod() => throw new ArgumentException();
1618+
}
1619+
""",
1620+
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
1621+
"""
1622+
using System;
1623+
using System.Threading.Tasks;
1624+
using TUnit.Core;
1625+
using TUnit.Assertions;
1626+
using static TUnit.Assertions.Assert;
1627+
using TUnit.Assertions.Extensions;
1628+
1629+
public class MyClass
1630+
{
1631+
[Test]
1632+
public async Task TestMethod()
1633+
{
1634+
await Assert.ThrowsAsync<ArgumentException>(async () => await SomeAsyncMethod());
1635+
}
1636+
1637+
private async System.Threading.Tasks.Task SomeAsyncMethod() => throw new ArgumentException();
1638+
}
1639+
""",
1640+
ConfigureNUnitTest
1641+
);
1642+
}
1643+
1644+
[Test]
1645+
public async Task NUnit_ThrowsAsync_WithConstraint_TypeOf_Converted()
1646+
{
1647+
await CodeFixer.VerifyCodeFixAsync(
1648+
"""
1649+
using NUnit.Framework;
1650+
using System;
1651+
1652+
{|#0:public class MyClass|}
1653+
{
1654+
[Test]
1655+
public void TestMethod()
1656+
{
1657+
Assert.ThrowsAsync(Is.TypeOf(typeof(ArgumentException)), async () => await SomeAsyncMethod());
1658+
}
1659+
1660+
private async System.Threading.Tasks.Task SomeAsyncMethod() => throw new ArgumentException();
1661+
}
1662+
""",
1663+
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
1664+
"""
1665+
using System;
1666+
using System.Threading.Tasks;
1667+
using TUnit.Core;
1668+
using TUnit.Assertions;
1669+
using static TUnit.Assertions.Assert;
1670+
using TUnit.Assertions.Extensions;
1671+
1672+
public class MyClass
1673+
{
1674+
[Test]
1675+
public async Task TestMethod()
1676+
{
1677+
await Assert.ThrowsAsync<ArgumentException>(async () => await SomeAsyncMethod());
1678+
}
1679+
1680+
private async System.Threading.Tasks.Task SomeAsyncMethod() => throw new ArgumentException();
1681+
}
1682+
""",
1683+
ConfigureNUnitTest
1684+
);
1685+
}
1686+
1687+
[Test]
1688+
public async Task NUnit_ThrowsAsync_WithConstraint_TypeOf_With_Code_To_Execute()
1689+
{
1690+
// This is the exact scenario from the bug report
1691+
await CodeFixer.VerifyCodeFixAsync(
1692+
"""
1693+
using NUnit.Framework;
1694+
using System;
1695+
1696+
{|#0:public class MyClass|}
1697+
{
1698+
[Test]
1699+
public void TestMethod()
1700+
{
1701+
var sut = new Sut();
1702+
Assert.ThrowsAsync(Is.TypeOf(typeof(ArgumentException)), async () => await sut.Execute(10));
1703+
}
1704+
}
1705+
1706+
public class Sut
1707+
{
1708+
public async System.Threading.Tasks.Task Execute(int value)
1709+
{
1710+
await System.Threading.Tasks.Task.Delay(1);
1711+
throw new ArgumentException();
1712+
}
1713+
}
1714+
""",
1715+
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
1716+
"""
1717+
using System;
1718+
using System.Threading.Tasks;
1719+
using TUnit.Core;
1720+
using TUnit.Assertions;
1721+
using static TUnit.Assertions.Assert;
1722+
using TUnit.Assertions.Extensions;
1723+
1724+
public class MyClass
1725+
{
1726+
[Test]
1727+
public async Task TestMethod()
1728+
{
1729+
var sut = new Sut();
1730+
await Assert.ThrowsAsync<ArgumentException>(async () => await sut.Execute(10));
1731+
}
1732+
}
1733+
1734+
public class Sut
1735+
{
1736+
public async System.Threading.Tasks.Task Execute(int value)
1737+
{
1738+
await System.Threading.Tasks.Task.Delay(1);
1739+
throw new ArgumentException();
1740+
}
1741+
}
1742+
""",
1743+
ConfigureNUnitTest
1744+
);
1745+
}
1746+
1747+
[Test]
1748+
public async Task NUnit_Throws_Generic_Converted()
1749+
{
1750+
await CodeFixer.VerifyCodeFixAsync(
1751+
"""
1752+
using NUnit.Framework;
1753+
using System;
1754+
1755+
{|#0:public class MyClass|}
1756+
{
1757+
[Test]
1758+
public void TestMethod()
1759+
{
1760+
Assert.Throws<ArgumentException>(() => throw new ArgumentException());
1761+
}
1762+
}
1763+
""",
1764+
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
1765+
"""
1766+
using System;
1767+
using System.Threading.Tasks;
1768+
using TUnit.Core;
1769+
using TUnit.Assertions;
1770+
using static TUnit.Assertions.Assert;
1771+
using TUnit.Assertions.Extensions;
1772+
1773+
public class MyClass
1774+
{
1775+
[Test]
1776+
public async Task TestMethod()
1777+
{
1778+
await Assert.ThrowsAsync<ArgumentException>(() => throw new ArgumentException());
1779+
}
1780+
}
1781+
""",
1782+
ConfigureNUnitTest
1783+
);
1784+
}
1785+
1786+
[Test]
1787+
public async Task NUnit_Throws_WithConstraint_TypeOf_Converted()
1788+
{
1789+
await CodeFixer.VerifyCodeFixAsync(
1790+
"""
1791+
using NUnit.Framework;
1792+
using System;
1793+
1794+
{|#0:public class MyClass|}
1795+
{
1796+
[Test]
1797+
public void TestMethod()
1798+
{
1799+
Assert.Throws(Is.TypeOf(typeof(ArgumentException)), () => throw new ArgumentException());
1800+
}
1801+
}
1802+
""",
1803+
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
1804+
"""
1805+
using System;
1806+
using System.Threading.Tasks;
1807+
using TUnit.Core;
1808+
using TUnit.Assertions;
1809+
using static TUnit.Assertions.Assert;
1810+
using TUnit.Assertions.Extensions;
1811+
1812+
public class MyClass
1813+
{
1814+
[Test]
1815+
public async Task TestMethod()
1816+
{
1817+
await Assert.ThrowsAsync<ArgumentException>(() => throw new ArgumentException());
1818+
}
1819+
}
1820+
""",
1821+
ConfigureNUnitTest
1822+
);
1823+
}
1824+
16011825
private static void ConfigureNUnitTest(Verifier.Test test)
16021826
{
16031827
test.TestState.AdditionalReferences.Add(typeof(NUnit.Framework.TestAttribute).Assembly);

0 commit comments

Comments
 (0)