Skip to content
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

S2699: Add UTs for Moq #9519

Merged
merged 3 commits into from
Jul 11, 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
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
Loading