Skip to content

Commit f7a5c04

Browse files
authored
137 xunit define codefixes for exceptionasserts assert.throws (#284)
* feat: add xunit Assert.Throws & Assert.ThrowsAsync * feat: add xunit Assert.ThrowsAny & Assert.ThrowsAnyAsync * feat: add xunit Assert.Throws*
1 parent 8af10d5 commit f7a5c04

File tree

6 files changed

+167
-40
lines changed

6 files changed

+167
-40
lines changed

src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,81 @@ public void AssertEquivalent_TestAnalyzer(string arguments, string assertion) =>
694694
public void AssertEquivalent_TestCodeFix(string oldAssertion, string newAssertion)
695695
=> VerifyCSharpFix("object actual, object expected", oldAssertion, newAssertion);
696696

697+
[DataTestMethod]
698+
[DataRow("Action action", "Assert.Throws(typeof(ArgumentException), action);")]
699+
[DataRow("Action action, Type exceptionType", "Assert.Throws(exceptionType, action);")]
700+
[DataRow("Action action", "Assert.Throws<NullReferenceException>(action);")]
701+
[DataRow("Action action", "Assert.Throws<ArgumentException>(\"propertyName\", action);")]
702+
[Implemented]
703+
public void AssertThrows_TestAnalyzer(string arguments, string assertion)
704+
=> VerifyCSharpDiagnostic(arguments, assertion);
705+
706+
[DataTestMethod]
707+
[DataRow("Action action",
708+
/* oldAssertion */ "Assert.Throws(typeof(ArgumentException), action);",
709+
/* newAssertion */ "action.Should().ThrowExactly<ArgumentException>();")]
710+
[DataRow("Action action",
711+
/* oldAssertion */ "Assert.Throws<NullReferenceException>(action);",
712+
/* newAssertion */ "action.Should().ThrowExactly<NullReferenceException>();")]
713+
[DataRow("Action action",
714+
/* oldAssertion */ "Assert.Throws<ArgumentException>(\"propertyName\", action);",
715+
/* newAssertion */ "action.Should().ThrowExactly<ArgumentException>().WithParameterName(\"propertyName\");")]
716+
[Implemented]
717+
public void AssertThrows_TestCodeFix(string arguments, string oldAssertion, string newAssertion)
718+
=> VerifyCSharpFix(arguments, oldAssertion, newAssertion);
719+
720+
[DataTestMethod]
721+
[DataRow("Func<Task> action", "Assert.ThrowsAsync(typeof(ArgumentException), action);")]
722+
[DataRow("Func<Task> action, Type exceptionType", "Assert.ThrowsAsync(exceptionType, action);")]
723+
[DataRow("Func<Task> action", "Assert.ThrowsAsync<NullReferenceException>(action);")]
724+
[DataRow("Func<Task> action", "Assert.ThrowsAsync<ArgumentException>(\"propertyName\", action);")]
725+
[Implemented]
726+
public void AssertThrowsAsync_TestAnalyzer(string arguments, string assertion)
727+
=> VerifyCSharpDiagnostic(arguments, assertion);
728+
729+
[DataTestMethod]
730+
[DataRow("Func<Task> action",
731+
/* oldAssertion */ "Assert.ThrowsAsync(typeof(ArgumentException), action);",
732+
/* newAssertion */ "action.Should().ThrowExactlyAsync<ArgumentException>();")]
733+
[DataRow("Func<Task> action",
734+
/* oldAssertion */ "Assert.ThrowsAsync<NullReferenceException>(action);",
735+
/* newAssertion */ "action.Should().ThrowExactlyAsync<NullReferenceException>();")]
736+
[DataRow("Func<Task> action",
737+
/* oldAssertion */ "Assert.ThrowsAsync<ArgumentException>(\"propertyName\", action);",
738+
/* newAssertion */ "action.Should().ThrowExactlyAsync<ArgumentException>().WithParameterName(\"propertyName\");")]
739+
[Implemented]
740+
public void AssertThrowsAsync_TestCodeFix(string arguments, string oldAssertion, string newAssertion)
741+
=> VerifyCSharpFix(arguments, oldAssertion, newAssertion);
742+
743+
[DataTestMethod]
744+
[DataRow("Action action", "Assert.ThrowsAny<NullReferenceException>(action);")]
745+
[Implemented]
746+
public void AssertThrowsAny_TestAnalyzer(string arguments, string assertion)
747+
=> VerifyCSharpDiagnostic(arguments, assertion);
748+
749+
[DataTestMethod]
750+
[DataRow("Action action",
751+
/* oldAssertion */ "Assert.ThrowsAny<NullReferenceException>(action);",
752+
/* newAssertion */ "action.Should().Throw<NullReferenceException>();")]
753+
[Implemented]
754+
public void AssertThrowsAny_TestCodeFix(string arguments, string oldAssertion, string newAssertion)
755+
=> VerifyCSharpFix(arguments, oldAssertion, newAssertion);
756+
757+
[DataTestMethod]
758+
[DataRow("Func<Task> action", "Assert.ThrowsAnyAsync<NullReferenceException>(action);")]
759+
[Implemented]
760+
public void AssertThrowsAnyAsync_TestAnalyzer(string arguments, string assertion)
761+
=> VerifyCSharpDiagnostic(arguments, assertion);
762+
763+
[DataTestMethod]
764+
[DataRow("Func<Task> action",
765+
/* oldAssertion */ "Assert.ThrowsAnyAsync<NullReferenceException>(action);",
766+
/* newAssertion */ "action.Should().ThrowAsync<NullReferenceException>();")]
767+
[Implemented]
768+
public void AssertThrowsAnyAsync_TestCodeFix(string arguments, string oldAssertion, string newAssertion)
769+
=> VerifyCSharpFix(arguments, oldAssertion, newAssertion);
770+
771+
697772
private void VerifyCSharpDiagnostic(string methodArguments, string assertion)
698773
{
699774
var source = GenerateCode.XunitAssertion(methodArguments, assertion);

src/FluentAssertions.Analyzers/Tips/DocumentEditorUtils.cs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ public class DocumentEditorUtils
1515
{
1616
public static CreateChangedDocument RenameMethodToSubjectShouldAssertion(IInvocationOperation invocation, CodeFixContext context, string newName, int subjectIndex, int[] argumentsToRemove)
1717
{
18-
var invocationExpression = (InvocationExpressionSyntax)invocation.Syntax;
19-
20-
return async ctx => await RewriteExpression(invocationExpression, [
21-
..Array.ConvertAll(argumentsToRemove, arg => EditAction.RemoveNode(invocationExpression.ArgumentList.Arguments[arg])),
18+
return async ctx => await RewriteExpression(invocation, [
19+
..Array.ConvertAll(argumentsToRemove, arg => EditAction.RemoveInvocationArgument(arg)),
2220
EditAction.SubjectShouldAssertion(subjectIndex, newName)
2321
], context, ctx);
2422
}
@@ -27,33 +25,39 @@ public static CreateChangedDocument RenameGenericMethodToSubjectShouldGenericAss
2725
=> RenameMethodToSubjectShouldGenericAssertion(invocation, invocation.TargetMethod.TypeArguments, context, newName, subjectIndex, argumentsToRemove);
2826
public static CreateChangedDocument RenameMethodToSubjectShouldGenericAssertion(IInvocationOperation invocation, ImmutableArray<ITypeSymbol> genericTypes, CodeFixContext context, string newName, int subjectIndex, int[] argumentsToRemove)
2927
{
30-
var invocationExpression = (InvocationExpressionSyntax)invocation.Syntax;
31-
32-
return async ctx => await RewriteExpression(invocationExpression, [
33-
..Array.ConvertAll(argumentsToRemove, arg => EditAction.RemoveNode(invocationExpression.ArgumentList.Arguments[arg])),
34-
EditAction.SubjectShouldGenericAssertion(subjectIndex, newName, genericTypes)
28+
return async ctx => await RewriteExpression(invocation, [
29+
..Array.ConvertAll(argumentsToRemove, arg => EditAction.RemoveInvocationArgument(arg)),
30+
EditAction.SubjectShouldGenericAssertion(subjectIndex, newName, genericTypes)
3531
], context, ctx);
3632
}
3733

3834
public static CreateChangedDocument RenameMethodToSubjectShouldAssertionWithOptionsLambda(IInvocationOperation invocation, CodeFixContext context, string newName, int subjectIndex, int optionsIndex)
3935
{
40-
var invocationExpression = (InvocationExpressionSyntax)invocation.Syntax;
41-
42-
return async ctx => await RewriteExpression(invocationExpression, [
36+
return async ctx => await RewriteExpression(invocation, [
4337
EditAction.SubjectShouldAssertion(subjectIndex, newName),
4438
EditAction.CreateEquivalencyAssertionOptionsLambda(optionsIndex)
4539
], context, ctx);
4640
}
4741

48-
private static async Task<Document> RewriteExpression(InvocationExpressionSyntax invocationExpression, Action<DocumentEditor, InvocationExpressionSyntax>[] actions, CodeFixContext context, CancellationToken cancellationToken)
42+
public static async Task<Document> RewriteExpression(IInvocationOperation invocation, Action<EditActionContext>[] actions, CodeFixContext context, CancellationToken cancellationToken)
4943
{
44+
var invocationExpression = (InvocationExpressionSyntax)invocation.Syntax;
45+
5046
var editor = await DocumentEditor.CreateAsync(context.Document, cancellationToken);
47+
var editActionContext = new EditActionContext(editor, invocationExpression);
5148

5249
foreach (var action in actions)
5350
{
54-
action(editor, invocationExpression);
51+
action(editActionContext);
5552
}
5653

5754
return editor.GetChangedDocument();
5855
}
5956
}
57+
58+
public class EditActionContext(DocumentEditor editor, InvocationExpressionSyntax invocationExpression) {
59+
public DocumentEditor Editor { get; } = editor;
60+
public InvocationExpressionSyntax InvocationExpression { get; } = invocationExpression;
61+
62+
public InvocationExpressionSyntax FluentAssertion { get; set; }
63+
}

src/FluentAssertions.Analyzers/Tips/Editing/EditAction.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ namespace FluentAssertions.Analyzers;
88

99
public static class EditAction
1010
{
11-
public static Action<DocumentEditor, InvocationExpressionSyntax> RemoveNode(SyntaxNode node)
12-
=> (editor, invocationExpression) => editor.RemoveNode(node);
11+
public static Action<EditActionContext> RemoveNode(SyntaxNode node)
12+
=> context => context.Editor.RemoveNode(node);
1313

14-
public static Action<DocumentEditor, InvocationExpressionSyntax> SubjectShouldAssertion(int argumentIndex, string assertion)
15-
=> (editor, invocationExpression) => new SubjectShouldAssertionAction(argumentIndex, assertion).Apply(editor, invocationExpression);
14+
public static Action<EditActionContext> RemoveInvocationArgument(int argumentIndex)
15+
=> context => context.Editor.RemoveNode(context.InvocationExpression.ArgumentList.Arguments[argumentIndex]);
1616

17-
public static Action<DocumentEditor, InvocationExpressionSyntax> SubjectShouldGenericAssertion(int argumentIndex, string assertion, ImmutableArray<ITypeSymbol> genericTypes)
18-
=> (editor, invocationExpression) => new SubjectShouldGenericAssertionAction(argumentIndex, assertion, genericTypes).Apply(editor, invocationExpression);
17+
public static Action<EditActionContext> SubjectShouldAssertion(int argumentIndex, string assertion)
18+
=> context => new SubjectShouldAssertionAction(argumentIndex, assertion).Apply(context);
1919

20-
public static Action<DocumentEditor, InvocationExpressionSyntax> CreateEquivalencyAssertionOptionsLambda(int optionsIndex)
21-
=> (editor, invocationExpression) => new CreateEquivalencyAssertionOptionsLambdaAction(optionsIndex).Apply(editor, invocationExpression);
20+
public static Action<EditActionContext> SubjectShouldGenericAssertion(int argumentIndex, string assertion, ImmutableArray<ITypeSymbol> genericTypes)
21+
=> context => new SubjectShouldGenericAssertionAction(argumentIndex, assertion, genericTypes).Apply(context);
22+
23+
public static Action<EditActionContext> CreateEquivalencyAssertionOptionsLambda(int optionsIndex)
24+
=> context => new CreateEquivalencyAssertionOptionsLambdaAction(optionsIndex).Apply(context.Editor, context.InvocationExpression);
2225
}

src/FluentAssertions.Analyzers/Tips/Editing/RemoveNodeAction.cs

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/FluentAssertions.Analyzers/Tips/Editing/SubjectShouldAssertionAction.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp;
23
using Microsoft.CodeAnalysis.CSharp.Syntax;
34
using Microsoft.CodeAnalysis.Editing;
45

56
namespace FluentAssertions.Analyzers;
67

7-
public class SubjectShouldAssertionAction : IEditAction
8+
public class SubjectShouldAssertionAction
89
{
910
private readonly int _argumentIndex;
1011
protected readonly string _assertion;
@@ -15,13 +16,21 @@ public SubjectShouldAssertionAction(int argumentIndex, string assertion)
1516
_assertion = assertion;
1617
}
1718

18-
public void Apply(DocumentEditor editor, InvocationExpressionSyntax invocationExpression)
19+
public void Apply(EditActionContext context)
1920
{
20-
var generator = editor.Generator;
21-
var subject = invocationExpression.ArgumentList.Arguments[_argumentIndex];
21+
var generator = context.Editor.Generator;
22+
var arguments = context.InvocationExpression.ArgumentList.Arguments;
23+
24+
var subject = arguments[_argumentIndex];
2225
var should = generator.InvocationExpression(generator.MemberAccessExpression(subject.Expression, "Should"));
23-
editor.RemoveNode(subject);
24-
editor.ReplaceNode(invocationExpression.Expression, generator.MemberAccessExpression(should, GenerateAssertion(generator)).WithTriviaFrom(invocationExpression.Expression));
26+
context.Editor.RemoveNode(subject);
27+
28+
var memberAccess = (MemberAccessExpressionSyntax) generator.MemberAccessExpression(should, GenerateAssertion(generator)).WithTriviaFrom(context.InvocationExpression.Expression);
29+
30+
context.Editor.ReplaceNode(context.InvocationExpression.Expression, memberAccess);
31+
context.FluentAssertion = context.InvocationExpression
32+
.WithExpression(memberAccess)
33+
.WithArgumentList(SyntaxFactory.ArgumentList(arguments.RemoveAt(_argumentIndex)));
2534
}
2635

2736
protected virtual SyntaxNode GenerateAssertion(SyntaxGenerator generator) => generator.IdentifierName(_assertion);

src/FluentAssertions.Analyzers/Tips/XunitCodeFixProvider.cs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
using System.Collections.Immutable;
22
using System.Composition;
3-
using System.Runtime.InteropServices.ComTypes;
43
using Microsoft.CodeAnalysis;
54
using Microsoft.CodeAnalysis.CodeFixes;
65
using Microsoft.CodeAnalysis.Operations;
76
using CreateChangedDocument = System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task<Microsoft.CodeAnalysis.Document>>;
7+
using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
88

99
namespace FluentAssertions.Analyzers;
1010

@@ -33,7 +33,8 @@ protected override CreateChangedDocument TryComputeFix(IInvocationOperation invo
3333
{
3434
if (invocation.Arguments[2].Value is ILiteralOperation literal)
3535
{
36-
return literal.ConstantValue.Value switch {
36+
return literal.ConstantValue.Value switch
37+
{
3738
false => DocumentEditorUtils.RenameMethodToSubjectShouldAssertion(invocation, context, "BeEquivalentTo", subjectIndex: 1, argumentsToRemove: literal.IsImplicit ? [] : [2]),
3839
_ => null
3940
};
@@ -150,7 +151,52 @@ protected override CreateChangedDocument TryComputeFix(IInvocationOperation invo
150151
return DocumentEditorUtils.RenameMethodToSubjectShouldAssertion(invocation, context, "BeInRange", subjectIndex: 0, argumentsToRemove: []);
151152
case "NotInRange" when ArgumentsCount(invocation, 3): // Assert.NotInRange<T>(T actual, T low, T high)
152153
return DocumentEditorUtils.RenameMethodToSubjectShouldAssertion(invocation, context, "NotBeInRange", subjectIndex: 0, argumentsToRemove: []);
154+
case "Throws" when ArgumentsCount(invocation, 1): // Assert.Throws<T>(Action testCode) where T : Exception
155+
return DocumentEditorUtils.RenameGenericMethodToSubjectShouldGenericAssertion(invocation, context, "ThrowExactly", subjectIndex: 0, argumentsToRemove: []);
156+
case "Throws" when ArgumentsAreTypeOf(invocation, t.Type, t.Action): // Assert.Throws(Type exceptionType, Action testCode)
157+
{
158+
if (invocation.Arguments[0].Value is not ITypeOfOperation typeOf)
159+
{
160+
return null; // no fix for this
161+
}
162+
163+
return DocumentEditorUtils.RenameMethodToSubjectShouldGenericAssertion(invocation, ImmutableArray.Create(typeOf.TypeOperand), context, "ThrowExactly", subjectIndex: 1, argumentsToRemove: [0]);
164+
}
165+
case "Throws" when ArgumentsAreTypeOf(invocation, t.String, t.Action): // Assert.Throws(string paramName, Action testCode)
166+
return RewriteThrowArgumentExceptionAssertion("ThrowExactly");
167+
case "ThrowsAsync" when ArgumentsCount(invocation, 1): // Assert.ThrowsAsync<T>(Func<Task> testCode) where T : Exception
168+
return DocumentEditorUtils.RenameGenericMethodToSubjectShouldGenericAssertion(invocation, context, "ThrowExactlyAsync", subjectIndex: 0, argumentsToRemove: []);
169+
case "ThrowsAsync" when ArgumentsAreTypeOf(invocation, t.Type, t.FuncOfTask): // Assert.ThrowsAsync(Type exceptionType, Func<Task> testCode)
170+
{
171+
if (invocation.Arguments[0].Value is not ITypeOfOperation typeOf)
172+
{
173+
return null; // no fix for this
174+
}
175+
176+
return DocumentEditorUtils.RenameMethodToSubjectShouldGenericAssertion(invocation, ImmutableArray.Create(typeOf.TypeOperand), context, "ThrowExactlyAsync", subjectIndex: 1, argumentsToRemove: [0]);
177+
}
178+
case "ThrowsAsync" when ArgumentsAreTypeOf(invocation, t.String, t.FuncOfTask): // Assert.ThrowsAsync(string paramName, Func<Task> testCode)
179+
return RewriteThrowArgumentExceptionAssertion("ThrowExactlyAsync");
180+
case "ThrowsAny" when ArgumentsCount(invocation, 1): // Assert.ThrowsAny<T>(Action testCode) where T : Exception
181+
return DocumentEditorUtils.RenameGenericMethodToSubjectShouldGenericAssertion(invocation, context, "Throw", subjectIndex: 0, argumentsToRemove: []);
182+
case "ThrowsAnyAsync" when ArgumentsCount(invocation, 1): // Assert.ThrowsAnyAsync<T>(Func<Task> testCode) where T : Exception
183+
return DocumentEditorUtils.RenameGenericMethodToSubjectShouldGenericAssertion(invocation, context, "ThrowAsync", subjectIndex: 0, argumentsToRemove: []);
153184
}
154185
return null;
186+
187+
CreateChangedDocument RewriteThrowArgumentExceptionAssertion(string newName)
188+
{
189+
return ctx => DocumentEditorUtils.RewriteExpression(invocation, [
190+
EditAction.SubjectShouldGenericAssertion(argumentIndex: 1, newName, invocation.TargetMethod.TypeArguments),
191+
(editActionContext) =>
192+
{
193+
var generator = editActionContext.Editor.Generator;
194+
var withParameterName = generator.MemberAccessExpression(editActionContext.FluentAssertion.WithArgumentList(SF.ArgumentList()), "WithParameterName");
195+
var chainedAssertion = generator.InvocationExpression(withParameterName, editActionContext.InvocationExpression.ArgumentList.Arguments[0]);
196+
197+
editActionContext.Editor.ReplaceNode(editActionContext.InvocationExpression, chainedAssertion);
198+
}
199+
], context, ctx);
200+
}
155201
}
156202
}

0 commit comments

Comments
 (0)