Skip to content

feat: support nunit assert.that migration #335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/NunitAnalyzer.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ var flag = true;
// old assertion:
Assert.IsTrue(flag);
Assert.True(flag);
Assert.That(flag);
Assert.That(flag, Is.True);
Assert.That(flag, Is.Not.False);

// new assertion:
flag.Should().BeTrue();
Expand All @@ -40,6 +43,15 @@ Assert.True(flag); /* fail message: Expected: True
Assert.IsTrue(flag); /* fail message: Expected: True
But was: False
*/
Assert.That(flag); /* fail message: Expected: True
But was: False
*/
Assert.That(flag, Is.True); /* fail message: Expected: True
But was: False
*/
Assert.That(flag, Is.Not.False); /* fail message: Expected: not False
But was: False
*/

// new assertion:
flag.Should().BeTrue(); /* fail message: Expected flag to be true, but found False. */
Expand All @@ -54,6 +66,8 @@ var flag = false;
// old assertion:
Assert.IsFalse(flag);
Assert.False(flag);
Assert.That(flag, Is.False);
Assert.That(flag, Is.Not.True);

// new assertion:
flag.Should().BeFalse();
Expand All @@ -71,6 +85,12 @@ Assert.False(flag); /* fail message: Expected: False
Assert.IsFalse(flag); /* fail message: Expected: False
But was: True
*/
Assert.That(flag, Is.False); /* fail message: Expected: False
But was: True
*/
Assert.That(flag, Is.Not.True); /* fail message: Expected: not True
But was: True
*/

// new assertion:
flag.Should().BeFalse(); /* fail message: Expected flag to be false, but found True. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Assert = NUnit.Framework.Assert;
using FluentAssertions;
using System.Collections.Generic;
using NUnit.Framework;
using FluentAssertions;

namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs;

Expand All @@ -17,6 +18,9 @@ public void BooleanAssertIsTrue()
// old assertion:
Assert.IsTrue(flag);
Assert.True(flag);
Assert.That(flag);
Assert.That(flag, Is.True);
Assert.That(flag, Is.Not.False);

// new assertion:
flag.Should().BeTrue();
Expand All @@ -42,6 +46,36 @@ public void BooleanAssertIsTrue_Failure_OldAssertion_1()
Assert.IsTrue(flag);
}

[TestMethod, ExpectedTestFrameworkException]
public void BooleanAssertIsTrue_Failure_OldAssertion_2()
{
// arrange
var flag = false;

// old assertion:
Assert.That(flag);
}

[TestMethod, ExpectedTestFrameworkException]
public void BooleanAssertIsTrue_Failure_OldAssertion_3()
{
// arrange
var flag = false;

// old assertion:
Assert.That(flag, Is.True);
}

[TestMethod, ExpectedTestFrameworkException]
public void BooleanAssertIsTrue_Failure_OldAssertion_4()
{
// arrange
var flag = false;

// old assertion:
Assert.That(flag, Is.Not.False);
}

[TestMethod, ExpectedTestFrameworkException]
public void BooleanAssertIsTrue_Failure_NewAssertion()
{
Expand All @@ -61,6 +95,8 @@ public void BooleanAssertIsFalse()
// old assertion:
Assert.IsFalse(flag);
Assert.False(flag);
Assert.That(flag, Is.False);
Assert.That(flag, Is.Not.True);

// new assertion:
flag.Should().BeFalse();
Expand All @@ -86,6 +122,26 @@ public void BooleanAssertIsFalse_Failure_OldAssertion_1()
Assert.IsFalse(flag);
}

[TestMethod, ExpectedTestFrameworkException]
public void BooleanAssertIsFalse_Failure_OldAssertion_2()
{
// arrange
var flag = true;

// old assertion:
Assert.That(flag, Is.False);
}

[TestMethod, ExpectedTestFrameworkException]
public void BooleanAssertIsFalse_Failure_OldAssertion_3()
{
// arrange
var flag = true;

// old assertion:
Assert.That(flag, Is.Not.True);
}

[TestMethod, ExpectedTestFrameworkException]
public void BooleanAssertIsFalse_Failure_NewAssertion()
{
Expand Down
12 changes: 12 additions & 0 deletions src/FluentAssertions.Analyzers.Tests/Tips/NunitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public class NunitTests
[AssertionDiagnostic("Assert.True(bool.Parse(\"true\"){0});")]
[AssertionDiagnostic("Assert.IsTrue(actual{0});")]
[AssertionDiagnostic("Assert.IsTrue(bool.Parse(\"true\"){0});")]
[AssertionDiagnostic("Assert.That(actual{0});")]
[AssertionDiagnostic("Assert.That(actual, Is.True{0});")]
[AssertionDiagnostic("Assert.That(actual, Is.Not.False{0});")]
[Implemented]
public void Nunit3_AssertTrue_TestAnalyzer(string assertion) => Nunit3VerifyDiagnostic("bool actual", assertion);

Expand Down Expand Up @@ -51,6 +54,15 @@ public class NunitTests
[AssertionCodeFix(
oldAssertion: "Assert.IsTrue(actual == false{0});",
newAssertion: "(actual == false).Should().BeTrue({0});")]
[AssertionCodeFix(
oldAssertion: "Assert.That(actual{0});",
newAssertion: "actual.Should().BeTrue({0});")]
[AssertionCodeFix(
oldAssertion: "Assert.That(actual, Is.True{0});",
newAssertion: "actual.Should().BeTrue({0});")]
[AssertionCodeFix(
oldAssertion: "Assert.That(actual, Is.Not.False{0});",
newAssertion: "actual.Should().BeTrue({0});")]
[Implemented]
public void Nunit3_AssertTrue_TestCodeFix(string oldAssertion, string newAssertion) => Nunit3VerifyFix("bool actual", oldAssertion, newAssertion);

Expand Down
45 changes: 42 additions & 3 deletions src/FluentAssertions.Analyzers/Tips/NunitCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
namespace FluentAssertions.Analyzers;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(NunitCodeFixProvider)), Shared]
public class NunitCodeFixProvider : TestingFrameworkCodeFixProvider
public class NunitCodeFixProvider : TestingFrameworkCodeFixProvider<NunitCodeFixProvider.NunitCodeFixContext>
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(AssertAnalyzer.NUnitRule.Id);
protected override CreateChangedDocument TryComputeFixCore(IInvocationOperation invocation, CodeFixContext context, TestingFrameworkCodeFixContext t, Diagnostic diagnostic)
protected override NunitCodeFixContext CreateTestContext(SemanticModel semanticModel) => new(semanticModel.Compilation);
protected override CreateChangedDocument TryComputeFixCore(IInvocationOperation invocation, CodeFixContext context, NunitCodeFixContext t, Diagnostic diagnostic)
{
var assertType = invocation.TargetMethod.ContainingType;
var nunitVersion = assertType.ContainingAssembly.Identity.Version;
Expand All @@ -24,6 +25,7 @@ protected override CreateChangedDocument TryComputeFixCore(IInvocationOperation

return assertType.Name switch
{
"Assert" when invocation.TargetMethod.Name is "That" => TryComputeFixForNunitThat(invocation, context, t),
"Assert" when isNunit3 => TryComputeFixForNunitClassicAssert(invocation, context, t),
"ClassicAssert" when isNunit4 => TryComputeFixForNunitClassicAssert(invocation, context, t),
//"StringAssert" => TryComputeFixForStringAssert(invocation, context, testContext),
Expand All @@ -32,7 +34,7 @@ protected override CreateChangedDocument TryComputeFixCore(IInvocationOperation
};
}

private CreateChangedDocument TryComputeFixForNunitClassicAssert(IInvocationOperation invocation, CodeFixContext context, TestingFrameworkCodeFixContext t)
private CreateChangedDocument TryComputeFixForNunitClassicAssert(IInvocationOperation invocation, CodeFixContext context, NunitCodeFixContext t)
{
switch (invocation.TargetMethod.Name)
{
Expand Down Expand Up @@ -228,4 +230,41 @@ private CreateChangedDocument TryComputeFixForNunitClassicAssert(IInvocationOper
}
return null;
}

private CreateChangedDocument TryComputeFixForNunitThat(IInvocationOperation invocation, CodeFixContext context, NunitCodeFixContext t)
{
if (invocation.Arguments.Length is 1 && invocation.Arguments[0].Value.Type.EqualsSymbol(t.Boolean) // Assert.That(subject)
|| invocation.Arguments.Length > 2 && invocation.Arguments[0].Value.Type.EqualsSymbol(t.Boolean) && invocation.Arguments[1].Value.Type.EqualsSymbol(t.String)) // Assert.That(subject, message)
{
return DocumentEditorUtils.RenameMethodToSubjectShouldAssertion(invocation, context, "BeTrue", subjectIndex: 0, argumentsToRemove: []);
}

if (invocation.Arguments[1].Value.UnwrapConversion() is not IPropertyReferenceOperation constraint) return null;

switch (constraint.Property.Name)
{
case "True" when constraint.Property.ContainingType.EqualsSymbol(t.Is): // Assert.That(subject, Is.True)
case "False" when constraint.Instance is IPropertyReferenceOperation { Property.Name: "Not" } chainedReference && PropertyReferencedFromType(chainedReference, t.Is): // Assert.That(subject, Is.Not.False)
return DocumentEditorUtils.RenameMethodToSubjectShouldAssertion(invocation, context, "BeTrue", subjectIndex: 0, argumentsToRemove: [1]);
case "True" when constraint.Instance is IPropertyReferenceOperation { Property.Name: "Not" } chainedReference && PropertyReferencedFromType(chainedReference, t.Is): // Assert.That(subject, Is.Not.True)
case "False" when PropertyReferencedFromType(constraint, t.Is): // Assert.That(subject, Is.False)
return DocumentEditorUtils.RenameMethodToSubjectShouldAssertion(invocation, context, "BeFalse", subjectIndex: 0, argumentsToRemove: [1]);

default:
return null;
}

}

private static bool PropertyReferencedFromType(IPropertyReferenceOperation propertyReference, INamedTypeSymbol type) => propertyReference.Property.ContainingType.EqualsSymbol(type);

public class NunitCodeFixContext(Compilation compilation) : TestingFrameworkCodeFixProvider.TestingFrameworkCodeFixContext(compilation)
{
public INamedTypeSymbol Is { get; } = compilation.GetTypeByMetadataName("NUnit.Framework.Is");
public INamedTypeSymbol Has { get; } = compilation.GetTypeByMetadataName("NUnit.Framework.Has");
public INamedTypeSymbol Does { get; } = compilation.GetTypeByMetadataName("NUnit.Framework.Does");
public INamedTypeSymbol Contains { get; } = compilation.GetTypeByMetadataName("NUnit.Framework.Contains");
public INamedTypeSymbol Throws { get; } = compilation.GetTypeByMetadataName("NUnit.Framework.Throws");
public INamedTypeSymbol ConstraintExpression { get; } = compilation.GetTypeByMetadataName("NUnit.Framework.Constraints.ConstraintExpression");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,49 @@
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Simplification;

namespace FluentAssertions.Analyzers;

public abstract class TestingFrameworkCodeFixProvider : CodeFixProviderBase<TestingFrameworkCodeFixProvider.TestingFrameworkCodeFixContext>
public abstract class TestingFrameworkCodeFixProvider<TTestContext> : CodeFixProviderBase<TTestContext> where TTestContext : TestingFrameworkCodeFixProvider.TestingFrameworkCodeFixContext
{
protected override string Title => "Replace with FluentAssertions";

protected override TestingFrameworkCodeFixContext CreateTestContext(SemanticModel semanticModel) => new TestingFrameworkCodeFixContext(semanticModel.Compilation);
protected override Func<CancellationToken, Task<Document>> TryComputeFix(IInvocationOperation invocation, CodeFixContext context, TTestContext t, Diagnostic diagnostic)
{
var fix = TryComputeFixCore(invocation, context, t, diagnostic);
if (fix is null)
{
return null;
}

return async ctx =>
{
const string fluentAssertionNamespace = "FluentAssertions";
var document = await fix(ctx);

var model = await document.GetSemanticModelAsync();
var scopes = model.GetImportScopes(diagnostic.Location.SourceSpan.Start);

var hasFluentAssertionImport = scopes.Any(scope => scope.Imports.Any(import => import.NamespaceOrType.ToString().Equals(fluentAssertionNamespace)));
if (hasFluentAssertionImport)
{
return document;
}

var root = (CompilationUnitSyntax)await document.GetSyntaxRootAsync();
root = root.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(fluentAssertionNamespace)).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation));

document = document.WithSyntaxRoot(root);
document = await Formatter.OrganizeImportsAsync(document);

return document;
};
}

protected abstract Func<CancellationToken, Task<Document>> TryComputeFixCore(IInvocationOperation invocation, CodeFixContext context, TTestContext t, Diagnostic diagnostic);

protected static bool ArgumentsAreTypeOf(IInvocationOperation invocation, params ITypeSymbol[] types) => ArgumentsAreTypeOf(invocation, 0, types);
protected static bool ArgumentsAreTypeOf(IInvocationOperation invocation, int startFromIndex, params ITypeSymbol[] types)
Expand Down Expand Up @@ -82,42 +112,14 @@ protected static bool ArgumentsCount(IInvocationOperation invocation, int argume
return invocation.TargetMethod.Parameters.Length == arguments;
}

protected override Func<CancellationToken, Task<Document>> TryComputeFix(IInvocationOperation invocation, CodeFixContext context, TestingFrameworkCodeFixContext t, Diagnostic diagnostic)
{
var fix = TryComputeFixCore(invocation, context, t, diagnostic);
if (fix is null)
{
return null;
}

return async ctx =>
{
const string fluentAssertionNamespace = "FluentAssertions";
var document = await fix(ctx);

var model = await document.GetSemanticModelAsync();
var scopes = model.GetImportScopes(diagnostic.Location.SourceSpan.Start);

var hasFluentAssertionImport = scopes.Any(scope => scope.Imports.Any(import => import.NamespaceOrType.ToString().Equals(fluentAssertionNamespace)));
if (hasFluentAssertionImport)
{
return document;
}

var root = (CompilationUnitSyntax) await document.GetSyntaxRootAsync();
root = root.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(fluentAssertionNamespace)).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation));
}

document = document.WithSyntaxRoot(root);
document = await Formatter.OrganizeImportsAsync(document);

return document;
};
}

protected abstract Func<CancellationToken, Task<Document>> TryComputeFixCore(IInvocationOperation invocation, CodeFixContext context, TestingFrameworkCodeFixContext t, Diagnostic diagnostic);
public abstract class TestingFrameworkCodeFixProvider : TestingFrameworkCodeFixProvider<TestingFrameworkCodeFixProvider.TestingFrameworkCodeFixContext>
{
protected override TestingFrameworkCodeFixContext CreateTestContext(SemanticModel semanticModel) => new(semanticModel.Compilation);


public sealed class TestingFrameworkCodeFixContext(Compilation compilation)
public class TestingFrameworkCodeFixContext(Compilation compilation)
{
public INamedTypeSymbol Object { get; } = compilation.ObjectType;
public INamedTypeSymbol String { get; } = compilation.GetTypeByMetadataName("System.String");
Expand Down
Loading