Skip to content

Commit

Permalink
S2699: Add UTs for Moq (#9519)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastien-marichal authored Jul 11, 2024
1 parent 605228e commit 7370c0b
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 141 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,140 +18,138 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

namespace SonarAnalyzer.Rules.CSharp
namespace SonarAnalyzer.Rules.CSharp;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class TestMethodShouldContainAssertion : SonarDiagnosticAnalyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class TestMethodShouldContainAssertion : SonarDiagnosticAnalyzer
{
internal const string DiagnosticId = "S2699";
private const string MessageFormat = "Add at least one assertion to this test case.";
private const string CustomAssertionAttributeName = "AssertionMethodAttribute";
private const int MaxInvocationDepth = 2; // Consider BFS instead of DFS if this gets increased
internal const string DiagnosticId = "S2699";
private const string MessageFormat = "Add at least one assertion to this test case.";
private const string CustomAssertionAttributeName = "AssertionMethodAttribute";
private const int MaxInvocationDepth = 2; // Consider BFS instead of DFS if this gets increased

private static readonly Dictionary<string, KnownType[]> KnownAssertions = new Dictionary<string, KnownType[]>
{
{"DidNotReceive", new[] {KnownType.NSubstitute_SubstituteExtensions}},
{"DidNotReceiveWithAnyArgs", new[] {KnownType.NSubstitute_SubstituteExtensions}},
{"Received", new[] {KnownType.NSubstitute_SubstituteExtensions, KnownType.NSubstitute_ReceivedExtensions_ReceivedExtensions}},
{"ReceivedWithAnyArgs", new[] {KnownType.NSubstitute_SubstituteExtensions, KnownType.NSubstitute_ReceivedExtensions_ReceivedExtensions}},
{"InOrder", new[] {KnownType.NSubstitute_Received}}
};

/// The assertions in the Shouldly library are supported by <see cref="UnitTestHelper.KnownAssertionMethodParts"/> (they all contain "Should")
private static readonly ImmutableArray<KnownType> KnownAssertionTypes = ImmutableArray.Create(
KnownType.Microsoft_VisualStudio_TestTools_UnitTesting_Assert,
KnownType.NFluent_Check,
KnownType.NUnit_Framework_Assert,
KnownType.Xunit_Assert);

private static readonly ImmutableArray<KnownType> KnownAsertionExceptionTypes = ImmutableArray.Create(
KnownType.Microsoft_VisualStudio_TestTools_UnitTesting_AssertFailedException,
KnownType.NFluent_FluentCheckException,
KnownType.NFluent_Kernel_FluentCheckException,
KnownType.NUnit_Framework_AssertionException,
KnownType.Xunit_Sdk_AssertException,
KnownType.Xunit_Sdk_XunitException);

private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

protected override void Initialize(SonarAnalysisContext context) =>
context.RegisterNodeAction(
c =>
{
var methodDeclaration = MethodDeclarationFactory.Create(c.Node);
if (!methodDeclaration.Identifier.IsMissing
&& methodDeclaration.HasImplementation
&& c.SemanticModel.GetDeclaredSymbol(c.Node) is IMethodSymbol methodSymbol
&& IsTestMethod(methodSymbol, methodDeclaration.IsLocal)
&& !methodSymbol.HasExpectedExceptionAttribute()
&& !methodSymbol.HasAssertionInAttribute()
&& !IsTestIgnored(methodSymbol)
&& !ContainsAssertion(c.Node, c.SemanticModel, new HashSet<IMethodSymbol>(), 0))
{
c.ReportIssue(Rule, methodDeclaration.Identifier);
}
},
SyntaxKind.MethodDeclaration,
SyntaxKindEx.LocalFunctionStatement);

// only xUnit allows local functions to be test methods.
private static bool IsTestMethod(IMethodSymbol symbol, bool isLocalFunction) =>
isLocalFunction ? IsXunitTestMethod(symbol) : symbol.IsTestMethod();

private static bool IsXunitTestMethod(IMethodSymbol methodSymbol) =>
methodSymbol.AnyAttributeDerivesFromAny(UnitTestHelper.KnownTestMethodAttributesOfxUnit);

private static bool ContainsAssertion(SyntaxNode methodDeclaration, SemanticModel previousSemanticModel, ISet<IMethodSymbol> visitedSymbols, int level)
{
var currentSemanticModel = methodDeclaration.EnsureCorrectSemanticModelOrDefault(previousSemanticModel);
if (currentSemanticModel == null)
private static readonly Dictionary<string, KnownType[]> KnownAssertions = new()
{
{"DidNotReceive", [KnownType.NSubstitute_SubstituteExtensions] },
{"DidNotReceiveWithAnyArgs", [KnownType.NSubstitute_SubstituteExtensions] },
{"Received", [KnownType.NSubstitute_SubstituteExtensions, KnownType.NSubstitute_ReceivedExtensions_ReceivedExtensions] },
{"ReceivedWithAnyArgs", [KnownType.NSubstitute_SubstituteExtensions, KnownType.NSubstitute_ReceivedExtensions_ReceivedExtensions] },
{"InOrder", [KnownType.NSubstitute_Received] }
};

/// The assertions in the Shouldly and Moq libraries are supported by <see cref="UnitTestHelper.KnownAssertionMethodParts"/>
/// - All assertions in Shouldly contain "Should" in their name.
/// - All assertions in Moq contain "Verify" in their name.
private static readonly ImmutableArray<KnownType> KnownAssertionTypes = ImmutableArray.Create(
KnownType.Microsoft_VisualStudio_TestTools_UnitTesting_Assert,
KnownType.NFluent_Check,
KnownType.NUnit_Framework_Assert,
KnownType.Xunit_Assert);

private static readonly ImmutableArray<KnownType> KnownAssertionExceptionTypes = ImmutableArray.Create(
KnownType.Microsoft_VisualStudio_TestTools_UnitTesting_AssertFailedException,
KnownType.NFluent_FluentCheckException,
KnownType.NFluent_Kernel_FluentCheckException,
KnownType.NUnit_Framework_AssertionException,
KnownType.Xunit_Sdk_AssertException,
KnownType.Xunit_Sdk_XunitException);

private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

protected override void Initialize(SonarAnalysisContext context) =>
context.RegisterNodeAction(
c =>
{
return false;
}
var methodDeclaration = MethodDeclarationFactory.Create(c.Node);
if (!methodDeclaration.Identifier.IsMissing
&& methodDeclaration.HasImplementation
&& c.SemanticModel.GetDeclaredSymbol(c.Node) is IMethodSymbol methodSymbol
&& IsTestMethod(methodSymbol, methodDeclaration.IsLocal)
&& !methodSymbol.HasExpectedExceptionAttribute()
&& !methodSymbol.HasAssertionInAttribute()
&& !IsTestIgnored(methodSymbol)
&& !ContainsAssertion(c.Node, c.SemanticModel, new HashSet<IMethodSymbol>(), 0))
{
c.ReportIssue(Rule, methodDeclaration.Identifier);
}
},
SyntaxKind.MethodDeclaration,
SyntaxKindEx.LocalFunctionStatement);

var descendantNodes = methodDeclaration.DescendantNodes();
var invocations = descendantNodes.OfType<InvocationExpressionSyntax>().ToArray();
if (invocations.Any(x => IsAssertion(x))
|| descendantNodes.OfType<ThrowStatementSyntax>().Any(x => x.Expression != null && currentSemanticModel.GetTypeInfo(x.Expression).Type.DerivesFromAny(KnownAsertionExceptionTypes)))
{
return true;
}
// only xUnit allows local functions to be test methods.
private static bool IsTestMethod(IMethodSymbol symbol, bool isLocalFunction) =>
isLocalFunction ? IsXunitTestMethod(symbol) : symbol.IsTestMethod();

var invokedSymbols = invocations.Select(expression => currentSemanticModel.GetSymbolInfo(expression).Symbol).OfType<IMethodSymbol>();
if (invokedSymbols.Any(symbol => IsKnownAssertion(symbol) || IsCustomAssertion(symbol)))
{
return true;
}
private static bool IsXunitTestMethod(IMethodSymbol methodSymbol) =>
methodSymbol.AnyAttributeDerivesFromAny(UnitTestHelper.KnownTestMethodAttributesOfxUnit);

if (level == MaxInvocationDepth)
{
return false;
}
private static bool ContainsAssertion(SyntaxNode methodDeclaration, SemanticModel model, ISet<IMethodSymbol> visitedSymbols, int level)
{
var currentModel = methodDeclaration.EnsureCorrectSemanticModelOrDefault(model);
if (currentModel is null)
{
return false;
}

foreach (var symbol in invokedSymbols.Where(x => !visitedSymbols.Contains(x)))
{
visitedSymbols.Add(symbol);
foreach (var invokedDeclaration in symbol.DeclaringSyntaxReferences.Select(x => x.GetSyntax()).OfType<MethodDeclarationSyntax>())
{
if (ContainsAssertion(invokedDeclaration, currentSemanticModel, visitedSymbols, level + 1))
{
return true;
}
}
}
var descendantNodes = methodDeclaration.DescendantNodes();
var invocations = descendantNodes.OfType<InvocationExpressionSyntax>().ToArray();
if (Array.Exists(invocations, IsAssertion)
|| descendantNodes.OfType<ThrowStatementSyntax>().Any(x => x.Expression is not null && currentModel.GetTypeInfo(x.Expression).Type.DerivesFromAny(KnownAssertionExceptionTypes)))
{
return true;
}

var invokedSymbols = invocations.Select(x => currentModel.GetSymbolInfo(x).Symbol).OfType<IMethodSymbol>();
if (invokedSymbols.Any(x => IsKnownAssertion(x) || IsCustomAssertion(x)))
{
return true;
}

if (level == MaxInvocationDepth)
{
return false;
}

private static bool IsTestIgnored(IMethodSymbol method)
foreach (var symbol in invokedSymbols.Where(x => !visitedSymbols.Contains(x)))
{
if (method.IsMsTestOrNUnitTestIgnored())
visitedSymbols.Add(symbol);
if (symbol.DeclaringSyntaxReferences.Select(x => x.GetSyntax()).OfType<MethodDeclarationSyntax>().Any(x => ContainsAssertion(x, currentModel, visitedSymbols, level + 1)))
{
return true;
}
}

// Checking whether an Xunit test is ignore or not needs to be done at the syntax level i.e. language-specific
var factAttributeSyntax = method.FindXUnitTestAttribute()
?.ApplicationSyntaxReference.GetSyntax() as AttributeSyntax;
return false;
}

return factAttributeSyntax?.ArgumentList != null
&& factAttributeSyntax.ArgumentList.Arguments.Any(x => x.NameEquals.Name.Identifier.ValueText == "Skip");
private static bool IsTestIgnored(IMethodSymbol method)
{
if (method.IsMsTestOrNUnitTestIgnored())
{
return true;
}

private static bool IsAssertion(InvocationExpressionSyntax invocation) =>
invocation.Expression
.ToString()
.SplitCamelCaseToWords()
.Intersect(UnitTestHelper.KnownAssertionMethodParts)
.Any();
// Checking whether an Xunit test is ignore or not needs to be done at the syntax level i.e. language-specific
var factAttributeSyntax = method.FindXUnitTestAttribute()
?.ApplicationSyntaxReference.GetSyntax() as AttributeSyntax;

private static bool IsKnownAssertion(ISymbol methodSymbol) =>
(KnownAssertions.GetValueOrDefault(methodSymbol.Name) is { } types && types.Any(x => methodSymbol.ContainingType.ConstructedFrom.Is(x)))
|| methodSymbol.ContainingType.DerivesFromAny(KnownAssertionTypes);

private static bool IsCustomAssertion(ISymbol methodSymbol) =>
methodSymbol.GetAttributesWithInherited().Any(x => x.AttributeClass.Name == CustomAssertionAttributeName);
return factAttributeSyntax?.ArgumentList is not null
&& factAttributeSyntax.ArgumentList.Arguments.Any(x => x.NameEquals.Name.Identifier.ValueText == "Skip");
}

private static bool IsAssertion(InvocationExpressionSyntax invocation) =>
invocation.Expression
.ToString()
.SplitCamelCaseToWords()
.Intersect(UnitTestHelper.KnownAssertionMethodParts)
.Any();

private static bool IsKnownAssertion(ISymbol methodSymbol) =>
(KnownAssertions.GetValueOrDefault(methodSymbol.Name) is { } types && Array.Exists(types, x => methodSymbol.ContainingType.ConstructedFrom.Is(x)))
|| methodSymbol.ContainingType.DerivesFromAny(KnownAssertionTypes);

private static bool IsCustomAssertion(ISymbol methodSymbol) =>
methodSymbol.GetAttributesWithInherited().Any(x => x.AttributeClass.Name == CustomAssertionAttributeName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,34 +100,35 @@ public void TestMethodShouldContainAssertion_Xunit_Legacy() =>
[DataRow(NUnitVersions.Ver25, FluentAssertionVersions.Ver1)]
[DataRow(NUnitVersions.Ver25, FluentAssertionVersions.Ver4)]
public void TestMethodShouldContainAssertion_NUnit_FluentAssertionsLegacy(string testFwkVersion, string fluentVersion) =>
WithTestReferences(NuGetMetadataReference.NUnit(testFwkVersion), fluentVersion).AddSnippet(@"
using System;
using FluentAssertions;
using NUnit.Framework;
WithTestReferences(NuGetMetadataReference.NUnit(testFwkVersion), fluentVersion).AddSnippet("""
using System;
using FluentAssertions;
using NUnit.Framework;

[TestFixture]
public class Foo
{
[Test]
public void Test1() // Noncompliant
{
var x = 42;
}
[TestFixture]
public class Foo
{
[Test]
public void Test1() // Noncompliant
{
var x = 42;
}

[Test]
public void ShouldThrowTest()
{
Action act = () => { throw new Exception(); };
act.ShouldThrow<Exception>();
}
[Test]
public void ShouldThrowTest()
{
Action act = () => { throw new Exception(); };
act.ShouldThrow<Exception>();
}

[Test]
public void ShouldNotThrowTest()
{
Action act = () => { throw new Exception(); };
act.ShouldNotThrow<Exception>();
}
}").Verify();
[Test]
public void ShouldNotThrowTest()
{
Action act = () => { throw new Exception(); };
act.ShouldNotThrow<Exception>();
}
}
""").Verify();

[TestMethod]
public void TestMethodShouldContainAssertion_NUnit_NFluentLegacy() =>
Expand All @@ -147,6 +148,10 @@ public void Test1()
}
""").VerifyNoIssues();

[TestMethod]
public void TestMethodShouldContainAssertion_Moq() =>
WithTestReferences(NuGetMetadataReference.MSTestTestFramework(Latest)).AddPaths("TestMethodShouldContainAssertion.Moq.cs").Verify();

[TestMethod]
public void TestMethodShouldContainAssertion_CustomAssertionMethod() =>
builder.AddPaths("TestMethodShouldContainAssertion.Custom.cs").AddReferences(NuGetMetadataReference.MSTestTestFramework(Latest)).Verify();
Expand Down Expand Up @@ -175,13 +180,15 @@ internal static VerifierBuilder WithTestReferences(IEnumerable<MetadataReference
string fluentVersion = Latest,
string nSubstituteVersion = Latest,
string nFluentVersion = Latest,
string shouldlyVersion = Latest) =>
string shouldlyVersion = Latest,
string moqVersion = Latest) =>
new VerifierBuilder<TestMethodShouldContainAssertion>()
.AddReferences(testFrameworkReference)
.AddReferences(NuGetMetadataReference.FluentAssertions(fluentVersion))
.AddReferences(NuGetMetadataReference.NSubstitute(nSubstituteVersion))
.AddReferences(NuGetMetadataReference.NFluent(nFluentVersion))
.AddReferences(NuGetMetadataReference.Shouldly(shouldlyVersion))
.AddReferences(NuGetMetadataReference.Moq(moqVersion))
.AddReferences(MetadataReferenceFacade.SystemData)
.AddReferences(MetadataReferenceFacade.SystemNetHttp)
.AddReferences(MetadataReferenceFacade.SystemXml)
Expand Down
Loading

0 comments on commit 7370c0b

Please sign in to comment.