diff --git a/TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs b/TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs
index 9cc05fda97..ea964fc8f2 100644
--- a/TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs
+++ b/TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs
@@ -34,6 +34,15 @@ protected ExpressionSyntax CreateTUnitAssertion(
string methodName,
ExpressionSyntax actualValue,
params ArgumentSyntax[] additionalArguments)
+ {
+ return CreateTUnitAssertionWithMessage(methodName, actualValue, null, additionalArguments);
+ }
+
+ protected ExpressionSyntax CreateTUnitAssertionWithMessage(
+ string methodName,
+ ExpressionSyntax actualValue,
+ ExpressionSyntax? message,
+ params ArgumentSyntax[] additionalArguments)
{
// Create Assert.That(actualValue)
var assertThatInvocation = SyntaxFactory.InvocationExpression(
@@ -60,11 +69,140 @@ protected ExpressionSyntax CreateTUnitAssertion(
? SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(additionalArguments))
: SyntaxFactory.ArgumentList();
- var fullInvocation = SyntaxFactory.InvocationExpression(methodAccess, arguments);
+ ExpressionSyntax fullInvocation = SyntaxFactory.InvocationExpression(methodAccess, arguments);
+
+ // Add .Because(message) if message is provided and non-empty
+ if (message != null && !IsEmptyOrNullMessage(message))
+ {
+ var becauseAccess = SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ fullInvocation,
+ SyntaxFactory.IdentifierName("Because")
+ );
- // Now wrap the entire thing in await: await Assert.That(actualValue).MethodName(args)
+ fullInvocation = SyntaxFactory.InvocationExpression(
+ becauseAccess,
+ SyntaxFactory.ArgumentList(
+ SyntaxFactory.SingletonSeparatedList(
+ SyntaxFactory.Argument(message)
+ )
+ )
+ );
+ }
+
+ // Now wrap the entire thing in await: await Assert.That(actualValue).MethodName(args).Because(message)
return SyntaxFactory.AwaitExpression(fullInvocation);
}
+
+ private static bool IsEmptyOrNullMessage(ExpressionSyntax message)
+ {
+ // Check for null literal
+ if (message is LiteralExpressionSyntax literal)
+ {
+ if (literal.IsKind(SyntaxKind.NullLiteralExpression))
+ {
+ return true;
+ }
+
+ // Check for empty string literal
+ if (literal.IsKind(SyntaxKind.StringLiteralExpression) &&
+ literal.Token.ValueText == "")
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Extracts the message and any format arguments from an argument list.
+ /// Format string messages like Assert.AreEqual(5, x, "Expected {0}", x) have args after the message.
+ ///
+ protected static (ExpressionSyntax? message, ArgumentSyntax[]? formatArgs) ExtractMessageWithFormatArgs(
+ SeparatedSyntaxList arguments,
+ int messageIndex)
+ {
+ if (arguments.Count <= messageIndex)
+ {
+ return (null, null);
+ }
+
+ var message = arguments[messageIndex].Expression;
+
+ // Check if there are additional format arguments after the message
+ if (arguments.Count > messageIndex + 1)
+ {
+ var formatArgs = arguments.Skip(messageIndex + 1).ToArray();
+ return (message, formatArgs);
+ }
+
+ return (message, null);
+ }
+
+ ///
+ /// Creates a message expression, wrapping in string.Format if format args are present.
+ ///
+ protected static ExpressionSyntax CreateMessageExpression(
+ ExpressionSyntax message,
+ ArgumentSyntax[]? formatArgs)
+ {
+ if (formatArgs == null || formatArgs.Length == 0)
+ {
+ return message;
+ }
+
+ // Create string.Format(message, arg1, arg2, ...)
+ var allArgs = new List
+ {
+ SyntaxFactory.Argument(message)
+ };
+ allArgs.AddRange(formatArgs);
+
+ return SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)),
+ SyntaxFactory.IdentifierName("Format")
+ ),
+ SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(allArgs))
+ );
+ }
+
+ ///
+ /// Checks if the argument at the given index appears to be a comparer (IComparer, IEqualityComparer).
+ ///
+ protected bool IsLikelyComparerArgument(ArgumentSyntax argument)
+ {
+ var typeInfo = SemanticModel.GetTypeInfo(argument.Expression);
+ if (typeInfo.Type == null) return false;
+
+ var typeName = typeInfo.Type.ToDisplayString();
+
+ // Check for IComparer, IComparer, IEqualityComparer, IEqualityComparer
+ if (typeName.Contains("IComparer") || typeName.Contains("IEqualityComparer"))
+ {
+ return true;
+ }
+
+ // Check interfaces
+ if (typeInfo.Type is INamedTypeSymbol namedType)
+ {
+ return namedType.AllInterfaces.Any(i =>
+ i.Name == "IComparer" ||
+ i.Name == "IEqualityComparer");
+ }
+
+ return false;
+ }
+
+ ///
+ /// Creates a TODO comment for unsupported features during migration.
+ ///
+ protected static SyntaxTrivia CreateTodoComment(string message)
+ {
+ return SyntaxFactory.Comment($"// TODO: TUnit migration - {message}");
+ }
protected bool IsFrameworkAssertion(InvocationExpressionSyntax invocation)
{
diff --git a/TUnit.Analyzers.CodeFixers/Base/AsyncMethodSignatureRewriter.cs b/TUnit.Analyzers.CodeFixers/Base/AsyncMethodSignatureRewriter.cs
new file mode 100644
index 0000000000..350227a03d
--- /dev/null
+++ b/TUnit.Analyzers.CodeFixers/Base/AsyncMethodSignatureRewriter.cs
@@ -0,0 +1,85 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace TUnit.Analyzers.CodeFixers.Base;
+
+///
+/// Transforms method signatures that contain await expressions but are not marked as async.
+/// Converts void methods to async Task and T-returning methods to async Task<T>.
+///
+public class AsyncMethodSignatureRewriter : CSharpSyntaxRewriter
+{
+ public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node)
+ {
+ // First, visit children to ensure nested content is processed
+ node = (MethodDeclarationSyntax)base.VisitMethodDeclaration(node)!;
+
+ // Skip if already async or abstract
+ if (node.Modifiers.Any(SyntaxKind.AsyncKeyword) ||
+ node.Modifiers.Any(SyntaxKind.AbstractKeyword))
+ {
+ return node;
+ }
+
+ // Check if method contains await expressions
+ bool hasAwait = node.DescendantNodes().OfType().Any();
+ if (!hasAwait)
+ {
+ return node;
+ }
+
+ // Convert the return type
+ var newReturnType = ConvertReturnType(node.ReturnType);
+
+ // Add async modifier after access modifiers but before other modifiers (like static)
+ var newModifiers = InsertAsyncModifier(node.Modifiers);
+
+ return node
+ .WithReturnType(newReturnType)
+ .WithModifiers(newModifiers);
+ }
+
+ private static TypeSyntax ConvertReturnType(TypeSyntax returnType)
+ {
+ // void -> Task
+ if (returnType is PredefinedTypeSyntax predefined && predefined.Keyword.IsKind(SyntaxKind.VoidKeyword))
+ {
+ return SyntaxFactory.ParseTypeName("Task")
+ .WithLeadingTrivia(returnType.GetLeadingTrivia())
+ .WithTrailingTrivia(returnType.GetTrailingTrivia());
+ }
+
+ // T -> Task
+ var innerType = returnType.WithoutTrivia();
+ return SyntaxFactory.GenericName("Task")
+ .WithTypeArgumentList(
+ SyntaxFactory.TypeArgumentList(
+ SyntaxFactory.SingletonSeparatedList(innerType)))
+ .WithLeadingTrivia(returnType.GetLeadingTrivia())
+ .WithTrailingTrivia(returnType.GetTrailingTrivia());
+ }
+
+ private static SyntaxTokenList InsertAsyncModifier(SyntaxTokenList modifiers)
+ {
+ // Find the right position for async (after public/private/etc, before static/virtual/etc)
+ int insertIndex = 0;
+
+ for (int i = 0; i < modifiers.Count; i++)
+ {
+ var modifier = modifiers[i];
+ if (modifier.IsKind(SyntaxKind.PublicKeyword) ||
+ modifier.IsKind(SyntaxKind.PrivateKeyword) ||
+ modifier.IsKind(SyntaxKind.ProtectedKeyword) ||
+ modifier.IsKind(SyntaxKind.InternalKeyword))
+ {
+ insertIndex = i + 1;
+ }
+ }
+
+ var asyncModifier = SyntaxFactory.Token(SyntaxKind.AsyncKeyword)
+ .WithTrailingTrivia(SyntaxFactory.Space);
+
+ return modifiers.Insert(insertIndex, asyncModifier);
+ }
+}
diff --git a/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs
index 5272c1796c..c76aaede50 100644
--- a/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs
@@ -57,6 +57,10 @@ protected async Task ConvertCodeAsync(Document document, SyntaxNode? r
// Framework-specific conversions (also use semantic model while it still matches)
compilationUnit = ApplyFrameworkSpecificConversions(compilationUnit, semanticModel, compilation);
+ // Fix method signatures that now contain await but aren't marked async
+ var asyncSignatureRewriter = new AsyncMethodSignatureRewriter();
+ compilationUnit = (CompilationUnitSyntax)asyncSignatureRewriter.Visit(compilationUnit);
+
// Remove unnecessary base classes and interfaces
var baseTypeRewriter = CreateBaseTypeRewriter(semanticModel, compilation);
compilationUnit = (CompilationUnitSyntax)baseTypeRewriter.Visit(compilationUnit);
@@ -69,6 +73,13 @@ protected async Task ConvertCodeAsync(Document document, SyntaxNode? r
var attributeRewriter = CreateAttributeRewriter(compilation);
compilationUnit = (CompilationUnitSyntax)attributeRewriter.Visit(compilationUnit);
+ // Ensure [Test] attribute is present when data attributes exist (NUnit-specific)
+ if (ShouldEnsureTestAttribute())
+ {
+ var testAttributeEnsurer = new TestAttributeEnsurer();
+ compilationUnit = (CompilationUnitSyntax)testAttributeEnsurer.Visit(compilationUnit);
+ }
+
// Remove framework usings and add TUnit usings (do this LAST)
compilationUnit = MigrationHelpers.RemoveFrameworkUsings(compilationUnit, FrameworkName);
@@ -106,6 +117,13 @@ protected async Task ConvertCodeAsync(Document document, SyntaxNode? r
///
protected virtual bool ShouldAddTUnitUsings() => true;
+ ///
+ /// Determines whether to run TestAttributeEnsurer to add [Test] when data attributes exist.
+ /// Override to return true for NUnit (where [TestCase] alone is valid but TUnit requires [Test] + [Arguments]).
+ /// Default is false since most frameworks don't need this.
+ ///
+ protected virtual bool ShouldEnsureTestAttribute() => false;
+
///
/// Removes excessive blank lines at the start of class members (after opening brace).
/// This can occur after removing members like ITestOutputHelper fields/properties.
diff --git a/TUnit.Analyzers.CodeFixers/Base/TestAttributeEnsurer.cs b/TUnit.Analyzers.CodeFixers/Base/TestAttributeEnsurer.cs
new file mode 100644
index 0000000000..ba0c546e87
--- /dev/null
+++ b/TUnit.Analyzers.CodeFixers/Base/TestAttributeEnsurer.cs
@@ -0,0 +1,136 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace TUnit.Analyzers.CodeFixers.Base;
+
+///
+/// Ensures that methods with data attributes (like [Arguments]) also have a [Test] attribute.
+/// This handles the case where NUnit allows [TestCase] alone, but TUnit requires [Test] + [Arguments].
+///
+public class TestAttributeEnsurer : CSharpSyntaxRewriter
+{
+ private static readonly string[] DataAttributeNames =
+ [
+ "Arguments",
+ "MethodDataSource",
+ "ClassDataSource",
+ "MatrixDataSource"
+ ];
+
+ public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node)
+ {
+ // First, visit children
+ node = (MethodDeclarationSyntax)base.VisitMethodDeclaration(node)!;
+
+ // Check if method has any data attributes
+ bool hasDataAttribute = HasAnyDataAttribute(node);
+ if (!hasDataAttribute)
+ {
+ return node;
+ }
+
+ // Check if method already has [Test] attribute
+ bool hasTestAttribute = HasTestAttribute(node);
+ if (hasTestAttribute)
+ {
+ return node;
+ }
+
+ // Add [Test] attribute before the first attribute list
+ return AddTestAttribute(node);
+ }
+
+ private static bool HasAnyDataAttribute(MethodDeclarationSyntax method)
+ {
+ foreach (var attributeList in method.AttributeLists)
+ {
+ foreach (var attribute in attributeList.Attributes)
+ {
+ var name = GetAttributeName(attribute);
+ if (DataAttributeNames.Contains(name))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static bool HasTestAttribute(MethodDeclarationSyntax method)
+ {
+ foreach (var attributeList in method.AttributeLists)
+ {
+ foreach (var attribute in attributeList.Attributes)
+ {
+ var name = GetAttributeName(attribute);
+ if (name == "Test")
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static string GetAttributeName(AttributeSyntax attribute)
+ {
+ var name = attribute.Name.ToString();
+
+ // Remove "Attribute" suffix if present
+ if (name.EndsWith("Attribute"))
+ {
+ name = name[..^9];
+ }
+
+ // Handle fully qualified names (take the last part)
+ var lastDot = name.LastIndexOf('.');
+ if (lastDot >= 0)
+ {
+ name = name[(lastDot + 1)..];
+ }
+
+ return name;
+ }
+
+ private static MethodDeclarationSyntax AddTestAttribute(MethodDeclarationSyntax method)
+ {
+ if (method.AttributeLists.Count == 0)
+ {
+ // No existing attributes - add [Test] with proper formatting
+ var testAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Test"));
+ var testAttributeList = SyntaxFactory.AttributeList(
+ SyntaxFactory.SingletonSeparatedList(testAttribute))
+ .WithTrailingTrivia(SyntaxFactory.EndOfLine("\n"));
+
+ return method.WithAttributeLists(
+ SyntaxFactory.SingletonList(testAttributeList));
+ }
+
+ // Get the leading trivia (indentation) from the first attribute list
+ var firstAttributeList = method.AttributeLists[0];
+ var leadingTrivia = firstAttributeList.GetLeadingTrivia();
+
+ // Create [Test] attribute list with same indentation
+ var testAttr = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Test"));
+ var newTestAttrList = SyntaxFactory.AttributeList(
+ SyntaxFactory.SingletonSeparatedList(testAttr))
+ .WithLeadingTrivia(leadingTrivia)
+ .WithTrailingTrivia(SyntaxFactory.EndOfLine("\n"));
+
+ // Strip newlines from first attribute's leading trivia, keep only indentation
+ // This prevents double newlines when we insert [Test] before it
+ var strippedTrivia = firstAttributeList.GetLeadingTrivia()
+ .Where(t => !t.IsKind(SyntaxKind.EndOfLineTrivia))
+ .ToList();
+ var updatedFirstAttr = firstAttributeList.WithLeadingTrivia(strippedTrivia);
+
+ // Build new attribute list: [Test], then updated first attr (with stripped trivia), then rest
+ var newAttributeLists = new SyntaxList()
+ .Add(newTestAttrList)
+ .Add(updatedFirstAttr)
+ .AddRange(method.AttributeLists.Skip(1));
+
+ return method.WithAttributeLists(newAttributeLists);
+ }
+}
diff --git a/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs
index 72a131101e..508048417d 100644
--- a/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs
@@ -217,83 +217,260 @@ protected override bool IsFrameworkAssertionNamespace(string namespaceName)
private ExpressionSyntax? ConvertMSTestAssertion(InvocationExpressionSyntax invocation, string methodName)
{
var arguments = invocation.ArgumentList.Arguments;
-
+
+ // MSTest assertion message parameter positions:
+ // - 2-arg assertions (IsTrue, IsFalse, IsNull, IsNotNull): message is 2nd param (index 1)
+ // - 3-arg assertions (AreEqual, AreSame, etc.): message is 3rd param (index 2)
+
return methodName switch
{
- "AreEqual" when arguments.Count >= 2 =>
+ // 2-arg assertions with message as 3rd param
+ "AreEqual" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("IsEqualTo", arguments[1].Expression, arguments[2].Expression, arguments[0]),
+ "AreEqual" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsEqualTo", arguments[1].Expression, arguments[0]),
- "AreNotEqual" when arguments.Count >= 2 =>
+ "AreNotEqual" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("IsNotEqualTo", arguments[1].Expression, arguments[2].Expression, arguments[0]),
+ "AreNotEqual" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsNotEqualTo", arguments[1].Expression, arguments[0]),
- "IsTrue" when arguments.Count >= 1 =>
+ "AreSame" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("IsSameReference", arguments[1].Expression, arguments[2].Expression, arguments[0]),
+ "AreSame" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("IsSameReference", arguments[1].Expression, arguments[0]),
+ "AreNotSame" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("IsNotSameReference", arguments[1].Expression, arguments[2].Expression, arguments[0]),
+ "AreNotSame" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("IsNotSameReference", arguments[1].Expression, arguments[0]),
+
+ // 1-arg assertions with message as 2nd param
+ "IsTrue" when arguments.Count >= 2 =>
+ CreateTUnitAssertionWithMessage("IsTrue", arguments[0].Expression, arguments[1].Expression),
+ "IsTrue" when arguments.Count >= 1 =>
CreateTUnitAssertion("IsTrue", arguments[0].Expression),
- "IsFalse" when arguments.Count >= 1 =>
+ "IsFalse" when arguments.Count >= 2 =>
+ CreateTUnitAssertionWithMessage("IsFalse", arguments[0].Expression, arguments[1].Expression),
+ "IsFalse" when arguments.Count >= 1 =>
CreateTUnitAssertion("IsFalse", arguments[0].Expression),
- "IsNull" when arguments.Count >= 1 =>
+ "IsNull" when arguments.Count >= 2 =>
+ CreateTUnitAssertionWithMessage("IsNull", arguments[0].Expression, arguments[1].Expression),
+ "IsNull" when arguments.Count >= 1 =>
CreateTUnitAssertion("IsNull", arguments[0].Expression),
- "IsNotNull" when arguments.Count >= 1 =>
+ "IsNotNull" when arguments.Count >= 2 =>
+ CreateTUnitAssertionWithMessage("IsNotNull", arguments[0].Expression, arguments[1].Expression),
+ "IsNotNull" when arguments.Count >= 1 =>
CreateTUnitAssertion("IsNotNull", arguments[0].Expression),
- "AreSame" when arguments.Count >= 2 =>
- CreateTUnitAssertion("IsSameReference", arguments[1].Expression, arguments[0]),
- "AreNotSame" when arguments.Count >= 2 =>
- CreateTUnitAssertion("IsNotSameReference", arguments[1].Expression, arguments[0]),
- "IsInstanceOfType" when arguments.Count >= 2 =>
+
+ // Type assertions with message as 3rd param
+ "IsInstanceOfType" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("IsAssignableTo", arguments[0].Expression, arguments[2].Expression, arguments[1]),
+ "IsInstanceOfType" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsAssignableTo", arguments[0].Expression, arguments[1]),
- "IsNotInstanceOfType" when arguments.Count >= 2 =>
+ "IsNotInstanceOfType" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("IsNotAssignableTo", arguments[0].Expression, arguments[2].Expression, arguments[1]),
+ "IsNotInstanceOfType" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsNotAssignableTo", arguments[0].Expression, arguments[1]),
+
+ // Special assertions
"ThrowsException" when arguments.Count >= 1 =>
CreateThrowsAssertion(invocation),
"ThrowsExceptionAsync" when arguments.Count >= 1 =>
CreateThrowsAsyncAssertion(invocation),
"Fail" => CreateFailAssertion(arguments),
+ "Inconclusive" => CreateInconclusiveAssertion(arguments),
_ => null
};
}
+
+ private ExpressionSyntax CreateInconclusiveAssertion(SeparatedSyntaxList arguments)
+ {
+ // Convert Assert.Inconclusive(message) to await Assert.Skip(message)
+ var skipInvocation = SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName("Assert"),
+ SyntaxFactory.IdentifierName("Skip")
+ ),
+ arguments.Count > 0
+ ? SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(arguments[0]))
+ : SyntaxFactory.ArgumentList()
+ );
+
+ return SyntaxFactory.AwaitExpression(skipInvocation);
+ }
private ExpressionSyntax? ConvertCollectionAssertion(InvocationExpressionSyntax invocation, string methodName)
{
var arguments = invocation.ArgumentList.Arguments;
-
+
+ // CollectionAssert message is typically the last parameter after the required args
+
return methodName switch
{
- "AreEqual" when arguments.Count >= 2 =>
+ // Equality assertions
+ "AreEqual" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("IsEquivalentTo", arguments[1].Expression, arguments[2].Expression, arguments[0]),
+ "AreEqual" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("IsEquivalentTo", arguments[1].Expression, arguments[0]),
+ "AreNotEqual" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("IsNotEquivalentTo", arguments[1].Expression, arguments[2].Expression, arguments[0]),
+ "AreNotEqual" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("IsNotEquivalentTo", arguments[1].Expression, arguments[0]),
+
+ // AreEquivalent (order independent)
+ "AreEquivalent" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("IsEquivalentTo", arguments[1].Expression, arguments[2].Expression, arguments[0]),
+ "AreEquivalent" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsEquivalentTo", arguments[1].Expression, arguments[0]),
- "AreNotEqual" when arguments.Count >= 2 =>
+ "AreNotEquivalent" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("IsNotEquivalentTo", arguments[1].Expression, arguments[2].Expression, arguments[0]),
+ "AreNotEquivalent" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsNotEquivalentTo", arguments[1].Expression, arguments[0]),
- "Contains" when arguments.Count >= 2 =>
+
+ // Contains assertions
+ "Contains" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("Contains", arguments[0].Expression, arguments[2].Expression, arguments[1]),
+ "Contains" when arguments.Count >= 2 =>
CreateTUnitAssertion("Contains", arguments[0].Expression, arguments[1]),
- "DoesNotContain" when arguments.Count >= 2 =>
+ "DoesNotContain" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("DoesNotContain", arguments[0].Expression, arguments[2].Expression, arguments[1]),
+ "DoesNotContain" when arguments.Count >= 2 =>
CreateTUnitAssertion("DoesNotContain", arguments[0].Expression, arguments[1]),
- "AllItemsAreNotNull" when arguments.Count >= 1 =>
- CreateTUnitAssertion("AllSatisfy", arguments[0].Expression,
- SyntaxFactory.Argument(
- SyntaxFactory.SimpleLambdaExpression(
- SyntaxFactory.Parameter(SyntaxFactory.Identifier("x")),
- SyntaxFactory.BinaryExpression(
- SyntaxKind.NotEqualsExpression,
- SyntaxFactory.IdentifierName("x"),
- SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)
- )
- )
- )),
+
+ // Subset/Superset
+ "IsSubsetOf" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("IsSubsetOf", arguments[0].Expression, arguments[2].Expression, arguments[1]),
+ "IsSubsetOf" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("IsSubsetOf", arguments[0].Expression, arguments[1]),
+ "IsNotSubsetOf" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("IsNotSubsetOf", arguments[0].Expression, arguments[2].Expression, arguments[1]),
+ "IsNotSubsetOf" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("IsNotSubsetOf", arguments[0].Expression, arguments[1]),
+
+ // Unique items
+ "AllItemsAreUnique" when arguments.Count >= 2 =>
+ CreateTUnitAssertionWithMessage("HasDistinctItems", arguments[0].Expression, arguments[1].Expression),
+ "AllItemsAreUnique" when arguments.Count >= 1 =>
+ CreateTUnitAssertion("HasDistinctItems", arguments[0].Expression),
+
+ // AllItemsAreNotNull
+ "AllItemsAreNotNull" when arguments.Count >= 2 =>
+ CreateAllItemsAreNotNullWithMessage(arguments[0].Expression, arguments[1].Expression),
+ "AllItemsAreNotNull" when arguments.Count >= 1 =>
+ CreateTUnitAssertion("AllSatisfy", arguments[0].Expression,
+ SyntaxFactory.Argument(CreateNotNullLambda())),
+
+ // AllItemsAreInstancesOfType
+ "AllItemsAreInstancesOfType" when arguments.Count >= 3 =>
+ CreateAllItemsAreInstancesOfTypeWithMessage(arguments[0].Expression, arguments[1].Expression, arguments[2].Expression),
+ "AllItemsAreInstancesOfType" when arguments.Count >= 2 =>
+ CreateAllItemsAreInstancesOfType(arguments[0].Expression, arguments[1].Expression),
+
_ => null
};
}
+
+ private ExpressionSyntax CreateAllItemsAreNotNullWithMessage(ExpressionSyntax collection, ExpressionSyntax message)
+ {
+ return CreateTUnitAssertionWithMessage("AllSatisfy", collection, message,
+ SyntaxFactory.Argument(CreateNotNullLambda()));
+ }
+
+ private static ExpressionSyntax CreateNotNullLambda()
+ {
+ return SyntaxFactory.SimpleLambdaExpression(
+ SyntaxFactory.Parameter(SyntaxFactory.Identifier("x")),
+ SyntaxFactory.BinaryExpression(
+ SyntaxKind.NotEqualsExpression,
+ SyntaxFactory.IdentifierName("x"),
+ SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)
+ )
+ );
+ }
+
+ private ExpressionSyntax CreateAllItemsAreInstancesOfType(ExpressionSyntax collection, ExpressionSyntax expectedType)
+ {
+ // Create a lambda: x => x.GetType() == expectedType or x is Type
+ var isExpression = SyntaxFactory.IsPatternExpression(
+ SyntaxFactory.IdentifierName("x"),
+ SyntaxFactory.DeclarationPattern(
+ SyntaxFactory.IdentifierName("_"),
+ SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier("_"))
+ )
+ );
+
+ // Simpler approach: use AllSatisfy with type check
+ // Since we have a Type argument, we'll create a comment explaining manual conversion needed
+ var result = CreateTUnitAssertion("AllSatisfy", collection,
+ SyntaxFactory.Argument(
+ SyntaxFactory.SimpleLambdaExpression(
+ SyntaxFactory.Parameter(SyntaxFactory.Identifier("x")),
+ SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ expectedType,
+ SyntaxFactory.IdentifierName("IsInstanceOfType")
+ ),
+ SyntaxFactory.ArgumentList(
+ SyntaxFactory.SingletonSeparatedList(
+ SyntaxFactory.Argument(SyntaxFactory.IdentifierName("x"))
+ )
+ )
+ )
+ )
+ ));
+ return result;
+ }
+
+ private ExpressionSyntax CreateAllItemsAreInstancesOfTypeWithMessage(ExpressionSyntax collection, ExpressionSyntax expectedType, ExpressionSyntax message)
+ {
+ var result = CreateTUnitAssertionWithMessage("AllSatisfy", collection, message,
+ SyntaxFactory.Argument(
+ SyntaxFactory.SimpleLambdaExpression(
+ SyntaxFactory.Parameter(SyntaxFactory.Identifier("x")),
+ SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ expectedType,
+ SyntaxFactory.IdentifierName("IsInstanceOfType")
+ ),
+ SyntaxFactory.ArgumentList(
+ SyntaxFactory.SingletonSeparatedList(
+ SyntaxFactory.Argument(SyntaxFactory.IdentifierName("x"))
+ )
+ )
+ )
+ )
+ ));
+ return result;
+ }
private ExpressionSyntax? ConvertStringAssertion(InvocationExpressionSyntax invocation, string methodName)
{
var arguments = invocation.ArgumentList.Arguments;
-
+
+ // StringAssert message is typically the last parameter after the required args
+
return methodName switch
{
- "Contains" when arguments.Count >= 2 =>
+ "Contains" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("Contains", arguments[0].Expression, arguments[2].Expression, arguments[1]),
+ "Contains" when arguments.Count >= 2 =>
CreateTUnitAssertion("Contains", arguments[0].Expression, arguments[1]),
- "DoesNotMatch" when arguments.Count >= 2 =>
+ "DoesNotMatch" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("DoesNotMatch", arguments[0].Expression, arguments[2].Expression, arguments[1]),
+ "DoesNotMatch" when arguments.Count >= 2 =>
CreateTUnitAssertion("DoesNotMatch", arguments[0].Expression, arguments[1]),
- "EndsWith" when arguments.Count >= 2 =>
+ "EndsWith" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("EndsWith", arguments[0].Expression, arguments[2].Expression, arguments[1]),
+ "EndsWith" when arguments.Count >= 2 =>
CreateTUnitAssertion("EndsWith", arguments[0].Expression, arguments[1]),
- "Matches" when arguments.Count >= 2 =>
+ "Matches" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("Matches", arguments[0].Expression, arguments[2].Expression, arguments[1]),
+ "Matches" when arguments.Count >= 2 =>
CreateTUnitAssertion("Matches", arguments[0].Expression, arguments[1]),
- "StartsWith" when arguments.Count >= 2 =>
+ "StartsWith" when arguments.Count >= 3 =>
+ CreateTUnitAssertionWithMessage("StartsWith", arguments[0].Expression, arguments[2].Expression, arguments[1]),
+ "StartsWith" when arguments.Count >= 2 =>
CreateTUnitAssertion("StartsWith", arguments[0].Expression, arguments[1]),
_ => null
};
diff --git a/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
index 9324db3cbb..f96f49210f 100644
--- a/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
@@ -43,6 +43,11 @@ protected override CompilationUnitSyntax ApplyFrameworkSpecificConversions(Compi
return compilationUnit;
}
+
+ ///
+ /// NUnit allows [TestCase] alone, but TUnit requires [Test] + [Arguments].
+ ///
+ protected override bool ShouldEnsureTestAttribute() => true;
}
public class NUnitAttributeRewriter : AttributeRewriter
@@ -158,27 +163,170 @@ private ExpressionSyntax ConvertAssertThat(InvocationExpressionSyntax invocation
var arguments = invocation.ArgumentList.Arguments;
var actualValue = arguments[0].Expression;
var constraint = arguments[1].Expression;
-
+
+ // Capture the optional message argument (3rd argument)
+ ExpressionSyntax? message = null;
+ if (arguments.Count >= 3)
+ {
+ message = arguments[2].Expression;
+ }
+
// Parse the constraint to determine the TUnit assertion method
if (constraint is InvocationExpressionSyntax constraintInvocation)
{
- return ConvertConstraintToTUnit(actualValue, constraintInvocation);
+ return ConvertConstraintToTUnitWithMessage(actualValue, constraintInvocation, message);
}
-
+
if (constraint is MemberAccessExpressionSyntax constraintMember)
{
- return ConvertConstraintMemberToTUnit(actualValue, constraintMember);
+ return ConvertConstraintMemberToTUnitWithMessage(actualValue, constraintMember, message);
}
-
- return CreateTUnitAssertion("IsEqualTo", actualValue, SyntaxFactory.Argument(constraint));
+
+ return CreateTUnitAssertionWithMessage("IsEqualTo", actualValue, message, SyntaxFactory.Argument(constraint));
}
-
+
+ private ExpressionSyntax ConvertConstraintToTUnitWithMessage(ExpressionSyntax actualValue, InvocationExpressionSyntax constraint, ExpressionSyntax? message)
+ {
+ if (constraint.Expression is MemberAccessExpressionSyntax memberAccess)
+ {
+ var methodName = memberAccess.Name.Identifier.Text;
+
+ // Handle Is.Not.EqualTo, Is.Not.GreaterThan, etc. (invocation patterns)
+ if (memberAccess.Expression is MemberAccessExpressionSyntax chainedAccess &&
+ chainedAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Is" } &&
+ chainedAccess.Name.Identifier.Text == "Not")
+ {
+ return methodName switch
+ {
+ "EqualTo" => CreateTUnitAssertionWithMessage("IsNotEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "GreaterThan" => CreateTUnitAssertionWithMessage("IsLessThanOrEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "LessThan" => CreateTUnitAssertionWithMessage("IsGreaterThanOrEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "GreaterThanOrEqualTo" => CreateTUnitAssertionWithMessage("IsLessThan", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "LessThanOrEqualTo" => CreateTUnitAssertionWithMessage("IsGreaterThan", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "SameAs" => CreateTUnitAssertionWithMessage("IsNotSameReferenceAs", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "InstanceOf" => CreateTUnitAssertionWithMessage("IsNotTypeOf", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ _ => CreateTUnitAssertionWithMessage("IsNotEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray())
+ };
+ }
+
+ // Handle Does.Not.StartWith, Does.Not.EndWith, Does.Not.Contain
+ if (memberAccess.Expression is MemberAccessExpressionSyntax doesNotAccess &&
+ doesNotAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Does" } &&
+ doesNotAccess.Name.Identifier.Text == "Not")
+ {
+ return methodName switch
+ {
+ "StartWith" => CreateTUnitAssertionWithMessage("DoesNotStartWith", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "EndWith" => CreateTUnitAssertionWithMessage("DoesNotEndWith", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "Contain" => CreateTUnitAssertionWithMessage("DoesNotContain", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ _ => CreateTUnitAssertionWithMessage("DoesNotContain", actualValue, message, constraint.ArgumentList.Arguments.ToArray())
+ };
+ }
+
+ // Handle Does.StartWith, Does.EndWith, Contains.Substring
+ if (memberAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Does" or "Contains" })
+ {
+ return methodName switch
+ {
+ "StartWith" => CreateTUnitAssertionWithMessage("StartsWith", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "EndWith" => CreateTUnitAssertionWithMessage("EndsWith", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "Substring" => CreateTUnitAssertionWithMessage("Contains", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ _ => CreateTUnitAssertionWithMessage("IsEqualTo", actualValue, message, SyntaxFactory.Argument(constraint))
+ };
+ }
+
+ return methodName switch
+ {
+ "EqualTo" => CreateTUnitAssertionWithMessage("IsEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "GreaterThan" => CreateTUnitAssertionWithMessage("IsGreaterThan", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "LessThan" => CreateTUnitAssertionWithMessage("IsLessThan", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "GreaterThanOrEqualTo" => CreateTUnitAssertionWithMessage("IsGreaterThanOrEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "LessThanOrEqualTo" => CreateTUnitAssertionWithMessage("IsLessThanOrEqualTo", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "Contains" => CreateTUnitAssertionWithMessage("Contains", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "StartsWith" => CreateTUnitAssertionWithMessage("StartsWith", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "EndsWith" => CreateTUnitAssertionWithMessage("EndsWith", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "SameAs" => CreateTUnitAssertionWithMessage("IsSameReferenceAs", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ "InstanceOf" => CreateTUnitAssertionWithMessage("IsTypeOf", actualValue, message, constraint.ArgumentList.Arguments.ToArray()),
+ _ => CreateTUnitAssertionWithMessage("IsEqualTo", actualValue, message, SyntaxFactory.Argument(constraint))
+ };
+ }
+
+ return CreateTUnitAssertionWithMessage("IsEqualTo", actualValue, message, SyntaxFactory.Argument(constraint));
+ }
+
+ private ExpressionSyntax ConvertConstraintMemberToTUnitWithMessage(ExpressionSyntax actualValue, MemberAccessExpressionSyntax constraint, ExpressionSyntax? message)
+ {
+ var memberName = constraint.Name.Identifier.Text;
+
+ // Handle Is.Not.X patterns (member access, not invocation)
+ if (constraint.Expression is MemberAccessExpressionSyntax innerMemberAccess &&
+ innerMemberAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Is" } &&
+ innerMemberAccess.Name.Identifier.Text == "Not")
+ {
+ return memberName switch
+ {
+ "Null" => CreateTUnitAssertionWithMessage("IsNotNull", actualValue, message),
+ "Empty" => CreateTUnitAssertionWithMessage("IsNotEmpty", actualValue, message),
+ "True" => CreateTUnitAssertionWithMessage("IsFalse", actualValue, message),
+ "False" => CreateTUnitAssertionWithMessage("IsTrue", actualValue, message),
+ "Positive" => CreateTUnitAssertionWithMessage("IsLessThanOrEqualTo", actualValue, message, SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)))),
+ "Negative" => CreateTUnitAssertionWithMessage("IsGreaterThanOrEqualTo", actualValue, message, SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)))),
+ "Zero" => CreateTUnitAssertionWithMessage("IsNotEqualTo", actualValue, message, SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)))),
+ _ => CreateTUnitAssertionWithMessage("IsNotEqualTo", actualValue, message, SyntaxFactory.Argument(constraint))
+ };
+ }
+
+ return memberName switch
+ {
+ "True" => CreateTUnitAssertionWithMessage("IsTrue", actualValue, message),
+ "False" => CreateTUnitAssertionWithMessage("IsFalse", actualValue, message),
+ "Null" => CreateTUnitAssertionWithMessage("IsNull", actualValue, message),
+ "Empty" => CreateTUnitAssertionWithMessage("IsEmpty", actualValue, message),
+ "Positive" => CreateTUnitAssertionWithMessage("IsPositive", actualValue, message),
+ "Negative" => CreateTUnitAssertionWithMessage("IsNegative", actualValue, message),
+ "Zero" => CreateTUnitAssertionWithMessage("IsZero", actualValue, message),
+ _ => CreateTUnitAssertionWithMessage("IsEqualTo", actualValue, message, SyntaxFactory.Argument(constraint))
+ };
+ }
+
private ExpressionSyntax ConvertConstraintToTUnit(ExpressionSyntax actualValue, InvocationExpressionSyntax constraint)
{
if (constraint.Expression is MemberAccessExpressionSyntax memberAccess)
{
var methodName = memberAccess.Name.Identifier.Text;
+ // Handle Is.Not.EqualTo, Is.Not.GreaterThan, etc. (invocation patterns)
+ if (memberAccess.Expression is MemberAccessExpressionSyntax chainedAccess &&
+ chainedAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Is" } &&
+ chainedAccess.Name.Identifier.Text == "Not")
+ {
+ return methodName switch
+ {
+ "EqualTo" => CreateTUnitAssertion("IsNotEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "GreaterThan" => CreateTUnitAssertion("IsLessThanOrEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "LessThan" => CreateTUnitAssertion("IsGreaterThanOrEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "GreaterThanOrEqualTo" => CreateTUnitAssertion("IsLessThan", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "LessThanOrEqualTo" => CreateTUnitAssertion("IsGreaterThan", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "SameAs" => CreateTUnitAssertion("IsNotSameReferenceAs", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "InstanceOf" => CreateTUnitAssertion("IsNotTypeOf", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ _ => CreateTUnitAssertion("IsNotEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray())
+ };
+ }
+
+ // Handle Does.Not.StartWith, Does.Not.EndWith, Does.Not.Contain
+ if (memberAccess.Expression is MemberAccessExpressionSyntax doesNotAccess &&
+ doesNotAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Does" } &&
+ doesNotAccess.Name.Identifier.Text == "Not")
+ {
+ return methodName switch
+ {
+ "StartWith" => CreateTUnitAssertion("DoesNotStartWith", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "EndWith" => CreateTUnitAssertion("DoesNotEndWith", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "Contain" => CreateTUnitAssertion("DoesNotContain", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ _ => CreateTUnitAssertion("DoesNotContain", actualValue, constraint.ArgumentList.Arguments.ToArray())
+ };
+ }
+
// Handle Does.StartWith, Does.EndWith, Contains.Substring
if (memberAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Does" or "Contains" })
{
@@ -196,9 +344,18 @@ private ExpressionSyntax ConvertConstraintToTUnit(ExpressionSyntax actualValue,
"EqualTo" => CreateTUnitAssertion("IsEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()),
"GreaterThan" => CreateTUnitAssertion("IsGreaterThan", actualValue, constraint.ArgumentList.Arguments.ToArray()),
"LessThan" => CreateTUnitAssertion("IsLessThan", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "GreaterThanOrEqualTo" => CreateTUnitAssertion("IsGreaterThanOrEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "LessThanOrEqualTo" => CreateTUnitAssertion("IsLessThanOrEqualTo", actualValue, constraint.ArgumentList.Arguments.ToArray()),
"Contains" => CreateTUnitAssertion("Contains", actualValue, constraint.ArgumentList.Arguments.ToArray()),
"StartsWith" => CreateTUnitAssertion("StartsWith", actualValue, constraint.ArgumentList.Arguments.ToArray()),
"EndsWith" => CreateTUnitAssertion("EndsWith", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "SameAs" => CreateTUnitAssertion("IsSameReferenceAs", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "InstanceOf" => CreateTUnitAssertion("IsTypeOf", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "SubsetOf" => CreateTUnitAssertion("IsSubsetOf", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "SupersetOf" => CreateTUnitAssertion("IsSupersetOf", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "EquivalentTo" => CreateTUnitAssertion("IsEquivalentTo", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "Matches" => CreateTUnitAssertion("Matches", actualValue, constraint.ArgumentList.Arguments.ToArray()),
+ "InRange" => CreateInRangeAssertion(actualValue, constraint.ArgumentList.Arguments),
_ => CreateTUnitAssertion("IsEqualTo", actualValue, SyntaxFactory.Argument(constraint))
};
}
@@ -210,7 +367,7 @@ private ExpressionSyntax ConvertConstraintMemberToTUnit(ExpressionSyntax actualV
{
var memberName = constraint.Name.Identifier.Text;
- // Handle Is.Not.X patterns
+ // Handle Is.Not.X patterns (member access, not invocation)
if (constraint.Expression is MemberAccessExpressionSyntax innerMemberAccess &&
innerMemberAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Is" } &&
innerMemberAccess.Name.Identifier.Text == "Not")
@@ -219,7 +376,12 @@ private ExpressionSyntax ConvertConstraintMemberToTUnit(ExpressionSyntax actualV
{
"Null" => CreateTUnitAssertion("IsNotNull", actualValue),
"Empty" => CreateTUnitAssertion("IsNotEmpty", actualValue),
- _ => CreateTUnitAssertion("IsEqualTo", actualValue, SyntaxFactory.Argument(constraint))
+ "True" => CreateTUnitAssertion("IsFalse", actualValue),
+ "False" => CreateTUnitAssertion("IsTrue", actualValue),
+ "Positive" => CreateTUnitAssertion("IsLessThanOrEqualTo", actualValue, SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)))),
+ "Negative" => CreateTUnitAssertion("IsGreaterThanOrEqualTo", actualValue, SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)))),
+ "Zero" => CreateTUnitAssertion("IsNotEqualTo", actualValue, SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)))),
+ _ => CreateTUnitAssertion("IsNotEqualTo", actualValue, SyntaxFactory.Argument(constraint))
};
}
@@ -229,39 +391,228 @@ private ExpressionSyntax ConvertConstraintMemberToTUnit(ExpressionSyntax actualV
"False" => CreateTUnitAssertion("IsFalse", actualValue),
"Null" => CreateTUnitAssertion("IsNull", actualValue),
"Empty" => CreateTUnitAssertion("IsEmpty", actualValue),
+ "Positive" => CreateTUnitAssertion("IsPositive", actualValue),
+ "Negative" => CreateTUnitAssertion("IsNegative", actualValue),
+ "Zero" => CreateTUnitAssertion("IsZero", actualValue),
+ "Unique" => CreateTUnitAssertion("HasDistinctItems", actualValue),
+ "Ordered" => CreateTUnitAssertion("IsInAscendingOrder", actualValue),
_ => CreateTUnitAssertion("IsEqualTo", actualValue, SyntaxFactory.Argument(constraint))
};
}
+
+ private ExpressionSyntax CreateInRangeAssertion(ExpressionSyntax actualValue, SeparatedSyntaxList arguments)
+ {
+ // Is.InRange(low, high) -> IsInRange(low, high)
+ if (arguments.Count >= 2)
+ {
+ return CreateTUnitAssertion("IsInRange", actualValue, arguments[0], arguments[1]);
+ }
+ return CreateTUnitAssertion("IsInRange", actualValue);
+ }
private ExpressionSyntax? ConvertClassicAssertion(InvocationExpressionSyntax invocation, string methodName)
{
var arguments = invocation.ArgumentList.Arguments;
-
+
+ // Handle Assert.Throws and Assert.ThrowsAsync first (generic methods)
+ if (methodName is "Throws" or "ThrowsAsync")
+ {
+ return ConvertNUnitThrows(invocation);
+ }
+
+ // Handle special assertions (Pass, Inconclusive, Fail, Warn)
return methodName switch
{
- "AreEqual" when arguments.Count >= 2 =>
- CreateTUnitAssertion("IsEqualTo", arguments[1].Expression, arguments[0]),
- "AreNotEqual" when arguments.Count >= 2 =>
- CreateTUnitAssertion("IsNotEqualTo", arguments[1].Expression, arguments[0]),
- "IsTrue" when arguments.Count >= 1 =>
- CreateTUnitAssertion("IsTrue", arguments[0].Expression),
- "IsFalse" when arguments.Count >= 1 =>
- CreateTUnitAssertion("IsFalse", arguments[0].Expression),
- "IsNull" when arguments.Count >= 1 =>
- CreateTUnitAssertion("IsNull", arguments[0].Expression),
- "IsNotNull" when arguments.Count >= 1 =>
- CreateTUnitAssertion("IsNotNull", arguments[0].Expression),
- "IsEmpty" when arguments.Count >= 1 =>
- CreateTUnitAssertion("IsEmpty", arguments[0].Expression),
- "IsNotEmpty" when arguments.Count >= 1 =>
- CreateTUnitAssertion("IsNotEmpty", arguments[0].Expression),
- "Greater" when arguments.Count >= 2 =>
- CreateTUnitAssertion("IsGreaterThan", arguments[0].Expression, arguments[1]),
- "Less" when arguments.Count >= 2 =>
- CreateTUnitAssertion("IsLessThan", arguments[0].Expression, arguments[1]),
+ // Pass and Fail
+ "Pass" => CreatePassAssertion(arguments),
+ "Fail" => CreateFailAssertion(arguments),
+ "Inconclusive" => CreateSkipAssertion(arguments),
+ "Ignore" => CreateSkipAssertion(arguments),
+
+ // 2-arg assertions (expected, actual) with optional message at index 2
+ "AreEqual" when arguments.Count >= 2 => ConvertAreEqualWithComparer(arguments),
+ "AreNotEqual" when arguments.Count >= 2 => ConvertAreNotEqualWithMessage(arguments),
+ "AreSame" when arguments.Count >= 2 => ConvertTwoArgWithMessage("IsSameReferenceAs", arguments),
+ "AreNotSame" when arguments.Count >= 2 => ConvertTwoArgWithMessage("IsNotSameReferenceAs", arguments),
+ "Greater" when arguments.Count >= 2 => ConvertTwoArgWithMessage("IsGreaterThan", arguments, swapArgs: false),
+ "GreaterOrEqual" when arguments.Count >= 2 => ConvertTwoArgWithMessage("IsGreaterThanOrEqualTo", arguments, swapArgs: false),
+ "Less" when arguments.Count >= 2 => ConvertTwoArgWithMessage("IsLessThan", arguments, swapArgs: false),
+ "LessOrEqual" when arguments.Count >= 2 => ConvertTwoArgWithMessage("IsLessThanOrEqualTo", arguments, swapArgs: false),
+
+ // 1-arg assertions with optional message at index 1
+ "IsTrue" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsTrue", arguments),
+ "IsFalse" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsFalse", arguments),
+ "IsNull" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsNull", arguments),
+ "IsNotNull" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsNotNull", arguments),
+ "IsEmpty" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsEmpty", arguments),
+ "IsNotEmpty" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsNotEmpty", arguments),
+ "IsNaN" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsNaN", arguments),
+ "IsInstanceOf" when arguments.Count >= 2 => ConvertInstanceOf(arguments, isNegated: false),
+ "IsNotInstanceOf" when arguments.Count >= 2 => ConvertInstanceOf(arguments, isNegated: true),
+
+ // Collection assertions
+ "Contains" when arguments.Count >= 2 => ConvertTwoArgWithMessage("Contains", arguments, swapArgs: false),
+
+ // Comparison assertions
+ "Positive" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsPositive", arguments),
+ "Negative" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsNegative", arguments),
+ "Zero" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsZero", arguments),
+ "NotZero" when arguments.Count >= 1 => ConvertOneArgWithMessage("IsNotZero", arguments),
+
_ => null
};
}
+
+ private ExpressionSyntax ConvertOneArgWithMessage(string methodName, SeparatedSyntaxList arguments)
+ {
+ var actualValue = arguments[0].Expression;
+ var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 1);
+ var messageExpr = message != null ? CreateMessageExpression(message, formatArgs) : null;
+ return CreateTUnitAssertionWithMessage(methodName, actualValue, messageExpr);
+ }
+
+ private ExpressionSyntax ConvertTwoArgWithMessage(string methodName, SeparatedSyntaxList arguments, bool swapArgs = true)
+ {
+ // For most NUnit assertions: expected is first, actual is second
+ // For TUnit: actual is first, expected goes in the method call
+ var actualValue = swapArgs ? arguments[1].Expression : arguments[0].Expression;
+ var expectedArg = swapArgs ? arguments[0] : arguments[1];
+ var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 2);
+ var messageExpr = message != null ? CreateMessageExpression(message, formatArgs) : null;
+ return CreateTUnitAssertionWithMessage(methodName, actualValue, messageExpr, expectedArg);
+ }
+
+ private ExpressionSyntax ConvertAreEqualWithComparer(SeparatedSyntaxList arguments)
+ {
+ var expected = arguments[0];
+ var actual = arguments[1].Expression;
+
+ // Check if 3rd argument is a comparer (not a string message)
+ if (arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2]))
+ {
+ // Add TODO comment and skip the comparer
+ var result = CreateTUnitAssertion("IsEqualTo", actual, expected);
+ return result.WithLeadingTrivia(
+ CreateTodoComment("custom comparer was used - consider using Assert.That(...).IsEquivalentTo() or a custom condition."),
+ SyntaxFactory.EndOfLine("\n"),
+ SyntaxFactory.Whitespace(" "));
+ }
+
+ var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 2);
+ var messageExpr = message != null ? CreateMessageExpression(message, formatArgs) : null;
+ return CreateTUnitAssertionWithMessage("IsEqualTo", actual, messageExpr, expected);
+ }
+
+ private ExpressionSyntax ConvertAreNotEqualWithMessage(SeparatedSyntaxList arguments)
+ {
+ var expected = arguments[0];
+ var actual = arguments[1].Expression;
+
+ // Check if 3rd argument is a comparer
+ if (arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2]))
+ {
+ var result = CreateTUnitAssertion("IsNotEqualTo", actual, expected);
+ return result.WithLeadingTrivia(
+ CreateTodoComment("custom comparer was used - consider using a custom condition."),
+ SyntaxFactory.EndOfLine("\n"),
+ SyntaxFactory.Whitespace(" "));
+ }
+
+ var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 2);
+ var messageExpr = message != null ? CreateMessageExpression(message, formatArgs) : null;
+ return CreateTUnitAssertionWithMessage("IsNotEqualTo", actual, messageExpr, expected);
+ }
+
+ private ExpressionSyntax ConvertInstanceOf(SeparatedSyntaxList arguments, bool isNegated)
+ {
+ var actualValue = arguments[0].Expression;
+ var expectedType = arguments[1];
+ var methodName = isNegated ? "IsNotAssignableTo" : "IsAssignableTo";
+ var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 2);
+ var messageExpr = message != null ? CreateMessageExpression(message, formatArgs) : null;
+ return CreateTUnitAssertionWithMessage(methodName, actualValue, messageExpr, expectedType);
+ }
+
+ private ExpressionSyntax ConvertNUnitThrows(InvocationExpressionSyntax invocation)
+ {
+ if (invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name is GenericNameSyntax genericName)
+ {
+ var exceptionType = genericName.TypeArgumentList.Arguments[0];
+ var action = invocation.ArgumentList.Arguments[0].Expression;
+
+ var throwsAsyncInvocation = SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName("Assert"),
+ SyntaxFactory.GenericName("ThrowsAsync")
+ .WithTypeArgumentList(
+ SyntaxFactory.TypeArgumentList(
+ SyntaxFactory.SingletonSeparatedList(exceptionType)
+ )
+ )
+ ),
+ SyntaxFactory.ArgumentList(
+ SyntaxFactory.SingletonSeparatedList(
+ SyntaxFactory.Argument(action)
+ )
+ )
+ );
+
+ return SyntaxFactory.AwaitExpression(throwsAsyncInvocation);
+ }
+
+ // Fallback for non-generic Throws
+ return CreateTUnitAssertion("Throws", invocation.ArgumentList.Arguments[0].Expression);
+ }
+
+ private ExpressionSyntax CreatePassAssertion(SeparatedSyntaxList arguments)
+ {
+ var passInvocation = SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName("Assert"),
+ SyntaxFactory.IdentifierName("Pass")
+ ),
+ arguments.Count > 0
+ ? SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(arguments[0]))
+ : SyntaxFactory.ArgumentList()
+ );
+
+ return SyntaxFactory.AwaitExpression(passInvocation);
+ }
+
+ private ExpressionSyntax CreateFailAssertion(SeparatedSyntaxList arguments)
+ {
+ var failInvocation = SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName("Assert"),
+ SyntaxFactory.IdentifierName("Fail")
+ ),
+ arguments.Count > 0
+ ? SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(arguments[0]))
+ : SyntaxFactory.ArgumentList()
+ );
+
+ return SyntaxFactory.AwaitExpression(failInvocation);
+ }
+
+ private ExpressionSyntax CreateSkipAssertion(SeparatedSyntaxList arguments)
+ {
+ var skipInvocation = SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName("Assert"),
+ SyntaxFactory.IdentifierName("Skip")
+ ),
+ arguments.Count > 0
+ ? SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(arguments[0]))
+ : SyntaxFactory.ArgumentList()
+ );
+
+ return SyntaxFactory.AwaitExpression(skipInvocation);
+ }
}
public class NUnitBaseTypeRewriter : CSharpSyntaxRewriter
diff --git a/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs
index 352417e2d8..02b2be8bdb 100644
--- a/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs
@@ -539,22 +539,39 @@ protected override bool IsFrameworkAssertionNamespace(string namespaceName)
return methodName switch
{
+ // Equality assertions - check for comparer overloads
+ "Equal" when arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2]) =>
+ CreateEqualWithComparerComment(arguments),
"Equal" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsEqualTo", arguments[1].Expression, arguments[0]),
+ "NotEqual" when arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2]) =>
+ CreateNotEqualWithComparerComment(arguments),
"NotEqual" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsNotEqualTo", arguments[1].Expression, arguments[0]),
+
+ // Boolean assertions
+ "True" when arguments.Count >= 2 =>
+ CreateTUnitAssertionWithMessage("IsTrue", arguments[0].Expression, arguments[1].Expression),
"True" when arguments.Count >= 1 =>
CreateTUnitAssertion("IsTrue", arguments[0].Expression),
+ "False" when arguments.Count >= 2 =>
+ CreateTUnitAssertionWithMessage("IsFalse", arguments[0].Expression, arguments[1].Expression),
"False" when arguments.Count >= 1 =>
CreateTUnitAssertion("IsFalse", arguments[0].Expression),
+
+ // Null assertions
"Null" when arguments.Count >= 1 =>
CreateTUnitAssertion("IsNull", arguments[0].Expression),
"NotNull" when arguments.Count >= 1 =>
CreateTUnitAssertion("IsNotNull", arguments[0].Expression),
+
+ // Reference assertions
"Same" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsSameReference", arguments[1].Expression, arguments[0]),
"NotSame" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsNotSameReference", arguments[1].Expression, arguments[0]),
+
+ // String/Collection contains
"Contains" when arguments.Count >= 2 =>
CreateTUnitAssertion("Contains", arguments[1].Expression, arguments[0]),
"DoesNotContain" when arguments.Count >= 2 =>
@@ -563,26 +580,154 @@ protected override bool IsFrameworkAssertionNamespace(string namespaceName)
CreateTUnitAssertion("StartsWith", arguments[1].Expression, arguments[0]),
"EndsWith" when arguments.Count >= 2 =>
CreateTUnitAssertion("EndsWith", arguments[1].Expression, arguments[0]),
+
+ // Empty/Not empty
"Empty" when arguments.Count >= 1 =>
CreateTUnitAssertion("IsEmpty", arguments[0].Expression),
"NotEmpty" when arguments.Count >= 1 =>
CreateTUnitAssertion("IsNotEmpty", arguments[0].Expression),
- "Throws" =>
- ConvertThrows(invocation, nameNode),
- "ThrowsAsync" =>
- ConvertThrowsAsync(invocation, nameNode),
- "IsType" =>
- ConvertIsType(invocation, nameNode),
- "IsAssignableFrom" =>
- ConvertIsAssignableFrom(invocation, nameNode),
+
+ // Exception assertions
+ "Throws" => ConvertThrows(invocation, nameNode),
+ "ThrowsAsync" => ConvertThrowsAsync(invocation, nameNode),
+ "ThrowsAny" => ConvertThrowsAny(invocation, nameNode),
+ "ThrowsAnyAsync" => ConvertThrowsAnyAsync(invocation, nameNode),
+
+ // Type assertions
+ "IsType" => ConvertIsType(invocation, nameNode),
+ "IsNotType" => ConvertIsNotType(invocation, nameNode),
+ "IsAssignableFrom" => ConvertIsAssignableFrom(invocation, nameNode),
+
+ // Range assertions
"InRange" when arguments.Count >= 3 =>
CreateTUnitAssertion("IsInRange", arguments[0].Expression, arguments[1], arguments[2]),
"NotInRange" when arguments.Count >= 3 =>
CreateTUnitAssertion("IsNotInRange", arguments[0].Expression, arguments[1], arguments[2]),
+
+ // Collection assertions
+ "Single" when arguments.Count >= 1 =>
+ CreateTUnitAssertion("HasSingleItem", arguments[0].Expression),
+ "All" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("AllSatisfy", arguments[0].Expression, arguments[1]),
+
+ // Subset/superset
+ "Subset" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("IsSubsetOf", arguments[0].Expression, arguments[1]),
+ "Superset" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("IsSupersetOf", arguments[0].Expression, arguments[1]),
+ "ProperSubset" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("IsSubsetOf", arguments[0].Expression, arguments[1]),
+ "ProperSuperset" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("IsSupersetOf", arguments[0].Expression, arguments[1]),
+
+ // Unique items
+ "Distinct" when arguments.Count >= 1 =>
+ CreateTUnitAssertion("HasDistinctItems", arguments[0].Expression),
+
+ // Equivalent (order independent)
+ "Equivalent" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("IsEquivalentTo", arguments[1].Expression, arguments[0]),
+
_ => null
};
}
+ private ExpressionSyntax CreateEqualWithComparerComment(SeparatedSyntaxList arguments)
+ {
+ var result = CreateTUnitAssertion("IsEqualTo", arguments[1].Expression, arguments[0]);
+ return result.WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - custom comparer was used. Consider using Assert.That(...).IsEquivalentTo() or a custom condition."),
+ SyntaxFactory.EndOfLine("\n"),
+ SyntaxFactory.Whitespace(" "));
+ }
+
+ private ExpressionSyntax CreateNotEqualWithComparerComment(SeparatedSyntaxList arguments)
+ {
+ var result = CreateTUnitAssertion("IsNotEqualTo", arguments[1].Expression, arguments[0]);
+ return result.WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - custom comparer was used. Consider using a custom condition."),
+ SyntaxFactory.EndOfLine("\n"),
+ SyntaxFactory.Whitespace(" "));
+ }
+
+ private ExpressionSyntax ConvertThrowsAny(InvocationExpressionSyntax invocation, SimpleNameSyntax nameNode)
+ {
+ // Assert.ThrowsAny(action) -> await Assert.ThrowsAsync(action)
+ // Note: ThrowsAny accepts derived types, ThrowsAsync should work similarly
+ if (nameNode is GenericNameSyntax genericName)
+ {
+ var exceptionType = genericName.TypeArgumentList.Arguments[0];
+ var action = invocation.ArgumentList.Arguments[0].Expression;
+
+ var invocationExpression = SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName("Assert"),
+ SyntaxFactory.GenericName("ThrowsAsync")
+ .WithTypeArgumentList(
+ SyntaxFactory.TypeArgumentList(
+ SyntaxFactory.SingletonSeparatedList(exceptionType)
+ )
+ )
+ ),
+ SyntaxFactory.ArgumentList(
+ SyntaxFactory.SingletonSeparatedList(
+ SyntaxFactory.Argument(action)
+ )
+ )
+ );
+
+ return SyntaxFactory.AwaitExpression(invocationExpression);
+ }
+
+ return CreateTUnitAssertion("Throws", invocation.ArgumentList.Arguments[0].Expression);
+ }
+
+ private ExpressionSyntax ConvertThrowsAnyAsync(InvocationExpressionSyntax invocation, SimpleNameSyntax nameNode)
+ {
+ // Same as ThrowsAny but for async
+ return ConvertThrowsAny(invocation, nameNode);
+ }
+
+ private ExpressionSyntax ConvertIsNotType(InvocationExpressionSyntax invocation, SimpleNameSyntax nameNode)
+ {
+ // Assert.IsNotType(value) -> await Assert.That(value).IsNotTypeOf()
+ if (nameNode is GenericNameSyntax genericName)
+ {
+ var expectedType = genericName.TypeArgumentList.Arguments[0];
+ var value = invocation.ArgumentList.Arguments[0].Expression;
+
+ var assertThatInvocation = SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName("Assert"),
+ SyntaxFactory.IdentifierName("That")
+ ),
+ SyntaxFactory.ArgumentList(
+ SyntaxFactory.SingletonSeparatedList(
+ SyntaxFactory.Argument(value)
+ )
+ )
+ );
+
+ var methodAccess = SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ assertThatInvocation,
+ SyntaxFactory.GenericName("IsNotTypeOf")
+ .WithTypeArgumentList(
+ SyntaxFactory.TypeArgumentList(
+ SyntaxFactory.SingletonSeparatedList(expectedType)
+ )
+ )
+ );
+
+ var fullInvocation = SyntaxFactory.InvocationExpression(methodAccess, SyntaxFactory.ArgumentList());
+ return SyntaxFactory.AwaitExpression(fullInvocation);
+ }
+
+ return CreateTUnitAssertion("IsNotTypeOf", invocation.ArgumentList.Arguments[0].Expression);
+ }
+
private ExpressionSyntax ConvertThrows(InvocationExpressionSyntax invocation, SimpleNameSyntax nameNode)
{
// Assert.Throws(action) -> await Assert.ThrowsAsync(action)
diff --git a/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs
index 4291101719..acdae641ae 100644
--- a/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs
+++ b/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs
@@ -122,7 +122,7 @@ public void MyMethod()
public class MyClass
{
[Test]
- public void MyMethod()
+ public async Task MyMethod()
{
await Assert.That(5).IsEqualTo(5);
await Assert.That(true).IsTrue();
@@ -293,7 +293,7 @@ public void MyMethod()
public class MyClass
{
[Test]
- public void MyMethod()
+ public async Task MyMethod()
{
var list1 = new[] { 1, 2, 3 };
var list2 = new[] { 1, 2, 3 };
@@ -334,7 +334,7 @@ public void StringTests()
public class MyClass
{
[Test]
- public void StringTests()
+ public async Task StringTests()
{
await Assert.That("hello world").Contains("world");
await Assert.That("hello world").StartsWith("hello");
@@ -383,14 +383,14 @@ public class OuterClass
public class InnerTests
{
[Test]
- public void InnerTest()
+ public async Task InnerTest()
{
await Assert.That(true).IsTrue();
}
}
[Test]
- public void OuterTest()
+ public async Task OuterTest()
{
await Assert.That(false).IsFalse();
}
@@ -427,7 +427,7 @@ public void GenericTest()
public class GenericTestClass
{
[Test]
- public void GenericTest()
+ public async Task GenericTest()
{
var instance = default(T);
await Assert.That(instance).IsEqualTo(default(T));
@@ -530,7 +530,7 @@ public void Setup()
}
[Test]
- public void Test1()
+ public async Task Test1()
{
await Assert.That(_counter > 0).IsTrue();
await Assert.That(_counter).IsNotNull();
@@ -539,7 +539,7 @@ public void Test1()
[Arguments(1, 2, 3)]
[Arguments(5, 5, 10)]
[Test]
- public void AdditionTest(int a, int b, int expected)
+ public async Task AdditionTest(int a, int b, int expected)
{
var result = a + b;
await Assert.That(result).IsEqualTo(expected);
@@ -547,7 +547,7 @@ public void AdditionTest(int a, int b, int expected)
[MethodDataSource(nameof(GetTestData))]
[Test]
- public void DataDrivenTest(string input)
+ public async Task DataDrivenTest(string input)
{
await Assert.That(input).IsNotNull();
}
@@ -616,7 +616,7 @@ public void TestMultipleAssertionTypes()
public class MyClass
{
[Test]
- public void TestMultipleAssertionTypes()
+ public async Task TestMultipleAssertionTypes()
{
var value = 42;
var list = new[] { 1, 2, 3 };
@@ -672,7 +672,7 @@ public void TestReferences()
public class MyClass
{
[Test]
- public void TestReferences()
+ public async Task TestReferences()
{
var obj1 = new object();
var obj2 = obj1;
@@ -687,6 +687,48 @@ public void TestReferences()
);
}
+ [Test]
+ public async Task MSTest_Assertion_Messages_Preserved()
+ {
+ await CodeFixer.VerifyCodeFixAsync(
+ """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ {|#0:public class MyClass|}
+ {
+ [TestMethod]
+ public void TestWithMessages()
+ {
+ Assert.AreEqual(5, 5, "Values should be equal");
+ Assert.IsTrue(true, "Should be true");
+ Assert.IsNull(null, "Should be null");
+ Assert.AreNotEqual(3, 5, "Values should not be equal");
+ }
+ }
+ """,
+ Verifier.Diagnostic(Rules.MSTestMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+ using TUnit.Assertions;
+ using static TUnit.Assertions.Assert;
+ using TUnit.Assertions.Extensions;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task TestWithMessages()
+ {
+ await Assert.That(5).IsEqualTo(5).Because("Values should be equal");
+ await Assert.That(true).IsTrue().Because("Should be true");
+ await Assert.That(null).IsNull().Because("Should be null");
+ await Assert.That(5).IsNotEqualTo(3).Because("Values should not be equal");
+ }
+ }
+ """,
+ ConfigureMSTestTest
+ );
+ }
+
private static void ConfigureMSTestTest(Verifier.Test test)
{
test.TestState.AdditionalReferences.Add(typeof(TestMethodAttribute).Assembly);
diff --git a/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs
index 9a28a47fc4..ce0a920c8c 100644
--- a/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs
+++ b/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs
@@ -29,12 +29,10 @@ public void MyMethod() { }
[Test]
[Arguments("NUnit.Framework.Test", "Test")]
- [Arguments("NUnit.Framework.TestCase(1, 2, 3)", "Arguments(1, 2, 3)")]
[Arguments("NUnit.Framework.SetUp", "Before(HookType.Test)")]
[Arguments("NUnit.Framework.TearDown", "After(HookType.Test)")]
[Arguments("NUnit.Framework.OneTimeSetUp", "Before(HookType.Class)")]
[Arguments("NUnit.Framework.OneTimeTearDown", "After(HookType.Class)")]
- [Arguments("NUnit.Framework.TestCaseSource(\"SomeMethod\")", "MethodDataSource(\"SomeMethod\")")]
public async Task NUnit_Attribute_Can_Be_Converted(string attribute, string expected)
{
await CodeFixer.VerifyCodeFixAsync(
@@ -123,7 +121,7 @@ public void MyMethod()
public class MyClass
{
[Test]
- public void MyMethod()
+ public async Task MyMethod()
{
await Assert.That(5).IsEqualTo(5);
await Assert.That(true).IsTrue();
@@ -134,7 +132,7 @@ public void MyMethod()
ConfigureNUnitTest
);
}
-
+
[Test]
public async Task NUnit_Classic_Assertions_Converted()
{
@@ -302,14 +300,14 @@ public class OuterClass
public class InnerTests
{
[Test]
- public void InnerTest()
+ public async Task InnerTest()
{
await Assert.That(true).IsTrue();
}
}
[Test]
- public void OuterTest()
+ public async Task OuterTest()
{
await Assert.That(false).IsFalse();
}
@@ -346,7 +344,7 @@ public void GenericTest()
public class GenericTestClass
{
[Test]
- public void GenericTest()
+ public async Task GenericTest()
{
var instance = default(T);
await Assert.That(instance).IsEqualTo(default(T));
@@ -388,7 +386,7 @@ public void ComplexConstraints()
public class MyClass
{
[Test]
- public void ComplexConstraints()
+ public async Task ComplexConstraints()
{
await Assert.That(10).IsGreaterThan(5);
await Assert.That(3).IsLessThan(10);
@@ -492,22 +490,24 @@ public void Setup()
}
[Test]
- public void Test1()
+ public async Task Test1()
{
await Assert.That(_counter).IsGreaterThan(0);
ClassicAssert.IsTrue(true);
}
+ [Test]
[Arguments(1, 2, 3)]
[Arguments(5, 5, 10)]
- public void AdditionTest(int a, int b, int expected)
+ public async Task AdditionTest(int a, int b, int expected)
{
var result = a + b;
await Assert.That(result).IsEqualTo(expected);
}
+ [Test]
[MethodDataSource(nameof(GetTestData))]
- public void DataDrivenTest(string input)
+ public async Task DataDrivenTest(string input)
{
await Assert.That(input).IsNotNull();
}
@@ -566,7 +566,7 @@ public void TestMultipleAssertions()
public class MyClass
{
[Test]
- public void TestMultipleAssertions()
+ public async Task TestMultipleAssertions()
{
var value = 42;
await Assert.That(value).IsNotNull();
@@ -728,6 +728,335 @@ public async Task ToUpper(string input, string expected)
);
}
+ [Test]
+ public async Task NUnit_IsNotEqualTo_Converted_Correctly()
+ {
+ await CodeFixer.VerifyCodeFixAsync(
+ """
+ using NUnit.Framework;
+
+ {|#0:public class MyClass|}
+ {
+ [Test]
+ public void TestMethod()
+ {
+ Assert.That(5, Is.Not.EqualTo(10));
+ }
+ }
+ """,
+ Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+ using TUnit.Assertions;
+ using static TUnit.Assertions.Assert;
+ using TUnit.Assertions.Extensions;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task TestMethod()
+ {
+ await Assert.That(5).IsNotEqualTo(10);
+ }
+ }
+ """,
+ ConfigureNUnitTest
+ );
+ }
+
+ [Test]
+ public async Task NUnit_IsNotGreaterThan_Converted_To_IsLessThanOrEqualTo()
+ {
+ await CodeFixer.VerifyCodeFixAsync(
+ """
+ using NUnit.Framework;
+
+ {|#0:public class MyClass|}
+ {
+ [Test]
+ public void TestMethod()
+ {
+ Assert.That(5, Is.Not.GreaterThan(10));
+ }
+ }
+ """,
+ Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+ using TUnit.Assertions;
+ using static TUnit.Assertions.Assert;
+ using TUnit.Assertions.Extensions;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task TestMethod()
+ {
+ await Assert.That(5).IsLessThanOrEqualTo(10);
+ }
+ }
+ """,
+ ConfigureNUnitTest
+ );
+ }
+
+ [Test]
+ public async Task NUnit_DoesNotContain_Converted_Correctly()
+ {
+ await CodeFixer.VerifyCodeFixAsync(
+ """
+ using NUnit.Framework;
+
+ {|#0:public class MyClass|}
+ {
+ [Test]
+ public void TestMethod()
+ {
+ Assert.That("hello world", Does.Not.Contain("foo"));
+ }
+ }
+ """,
+ Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+ using TUnit.Assertions;
+ using static TUnit.Assertions.Assert;
+ using TUnit.Assertions.Extensions;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task TestMethod()
+ {
+ await Assert.That("hello world").DoesNotContain("foo");
+ }
+ }
+ """,
+ ConfigureNUnitTest
+ );
+ }
+
+ [Test]
+ public async Task NUnit_DoesNotStartWith_Converted_Correctly()
+ {
+ await CodeFixer.VerifyCodeFixAsync(
+ """
+ using NUnit.Framework;
+
+ {|#0:public class MyClass|}
+ {
+ [Test]
+ public void TestMethod()
+ {
+ Assert.That("hello world", Does.Not.StartWith("foo"));
+ }
+ }
+ """,
+ Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+ using TUnit.Assertions;
+ using static TUnit.Assertions.Assert;
+ using TUnit.Assertions.Extensions;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task TestMethod()
+ {
+ await Assert.That("hello world").DoesNotStartWith("foo");
+ }
+ }
+ """,
+ ConfigureNUnitTest
+ );
+ }
+
+ [Test]
+ public async Task NUnit_AssertionMessage_Converted_To_Because()
+ {
+ await CodeFixer.VerifyCodeFixAsync(
+ """
+ using NUnit.Framework;
+
+ {|#0:public class MyClass|}
+ {
+ [Test]
+ public void TestMethod()
+ {
+ Assert.That(5, Is.EqualTo(5), "Values should match");
+ }
+ }
+ """,
+ Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+ using TUnit.Assertions;
+ using static TUnit.Assertions.Assert;
+ using TUnit.Assertions.Extensions;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task TestMethod()
+ {
+ await Assert.That(5).IsEqualTo(5).Because("Values should match");
+ }
+ }
+ """,
+ ConfigureNUnitTest
+ );
+ }
+
+ [Test]
+ public async Task NUnit_AssertionMessage_WithNegation_Converted_To_Because()
+ {
+ await CodeFixer.VerifyCodeFixAsync(
+ """
+ using NUnit.Framework;
+
+ {|#0:public class MyClass|}
+ {
+ [Test]
+ public void TestMethod()
+ {
+ Assert.That(5, Is.Not.EqualTo(10), "Hash should differ");
+ }
+ }
+ """,
+ Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+ using TUnit.Assertions;
+ using static TUnit.Assertions.Assert;
+ using TUnit.Assertions.Extensions;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task TestMethod()
+ {
+ await Assert.That(5).IsNotEqualTo(10).Because("Hash should differ");
+ }
+ }
+ """,
+ ConfigureNUnitTest
+ );
+ }
+
+ [Test]
+ public async Task NUnit_VoidMethod_ConvertedToAsyncTask_WhenContainsAwait()
+ {
+ await CodeFixer.VerifyCodeFixAsync(
+ """
+ using NUnit.Framework;
+
+ {|#0:public class MyClass|}
+ {
+ [Test]
+ public void TestMethod()
+ {
+ Assert.That(true, Is.True);
+ }
+ }
+ """,
+ Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+ using TUnit.Assertions;
+ using static TUnit.Assertions.Assert;
+ using TUnit.Assertions.Extensions;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task TestMethod()
+ {
+ await Assert.That(true).IsTrue();
+ }
+ }
+ """,
+ ConfigureNUnitTest
+ );
+ }
+
+ [Test]
+ public async Task NUnit_TestCaseOnly_AddsTestAttribute()
+ {
+ await CodeFixer.VerifyCodeFixAsync(
+ """
+ using NUnit.Framework;
+
+ {|#0:public class MyClass|}
+ {
+ [TestCase(1, 2, 3)]
+ [TestCase(4, 5, 9)]
+ public void AdditionTest(int a, int b, int expected)
+ {
+ Assert.That(a + b, Is.EqualTo(expected));
+ }
+ }
+ """,
+ Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+ using TUnit.Assertions;
+ using static TUnit.Assertions.Assert;
+ using TUnit.Assertions.Extensions;
+
+ public class MyClass
+ {
+ [Test]
+ [Arguments(1, 2, 3)]
+ [Arguments(4, 5, 9)]
+ public async Task AdditionTest(int a, int b, int expected)
+ {
+ await Assert.That(a + b).IsEqualTo(expected);
+ }
+ }
+ """,
+ ConfigureNUnitTest
+ );
+ }
+
+ [Test]
+ public async Task NUnit_TestCaseWithExistingTest_DoesNotDuplicateTestAttribute()
+ {
+ await CodeFixer.VerifyCodeFixAsync(
+ """
+ using NUnit.Framework;
+
+ {|#0:public class MyClass|}
+ {
+ [Test]
+ [TestCase(1, 2, 3)]
+ public void AdditionTest(int a, int b, int expected)
+ {
+ Assert.That(a + b, Is.EqualTo(expected));
+ }
+ }
+ """,
+ Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+ using TUnit.Assertions;
+ using static TUnit.Assertions.Assert;
+ using TUnit.Assertions.Extensions;
+
+ public class MyClass
+ {
+ [Test]
+ [Arguments(1, 2, 3)]
+ public async Task AdditionTest(int a, int b, int expected)
+ {
+ await Assert.That(a + b).IsEqualTo(expected);
+ }
+ }
+ """,
+ ConfigureNUnitTest
+ );
+ }
+
private static void ConfigureNUnitTest(Verifier.Test test)
{
test.TestState.AdditionalReferences.Add(typeof(NUnit.Framework.TestAttribute).Assembly);