Giter Site home page Giter Site logo

moq.analyzers's Introduction

Moq.Analyzers

NuGet Version NuGet Downloads Main build Codacy Grade Badge Codacy Coverage Badge

Moq.Analyzers is a Roslyn analyzer that helps you to write unit tests using the popular Moq framework. Moq.Analyzers protects you from common mistakes and warns you if something is wrong with your Moq configuration.

Analyzer rules

  • Moq1000: Sealed classes cannot be mocked
  • Moq1001: Mocked interfaces cannot have constructor parameters
  • Moq1002: Parameters provided into mock do not match any existing constructors
  • Moq1100: Callback signature must match the signature of the mocked method
  • Moq1101: SetupGet/SetupSet should be used for properties, not for methods
  • Moq1200: Setup should be used only for overridable members
  • Moq1201: Setup of async methods should use .ReturnsAsync instance instead of .Result
  • Moq1300: Mock.As() should take interfaces

See docs/rules for full documentation.

Getting started

Moq.Analyzers is installed from NuGet. Run this command for your test project(s):

dotnet add package Moq.Analyzers

NOTE: You must use a supported version of the .NET SDK (i.e. 6.0 or later).

Contributions welcome

Moq.Analyzers continues to evolve and add new features. Any help will be appreciated. You can report issues, develop new features, improve the documentation, or do other cool stuff. See CONTRIBUTING.md.

moq.analyzers's People

Contributors

cezarypiatek avatar dependabot[bot] avatar litee avatar mattkotsenas avatar rjmurillo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

moq.analyzers's Issues

Explainer: Refactor to IOperation

Refactor to IOperation explained

What's All This About?

The code analyzers and fixes in this repo directly use the C# syntax tree and the semantic model to detect some patterns and report warnings. This means the analyzers cannot be used for other .NET languages, as the syntax tree is different. This means for each language to be supported, a new analyzer must be duplicated, which increases complexity and maintenance costs. Additionally, the computational cost of traversing the syntax tree is expensive. Moving to IOperation reduces the complexity and makes the analyzers easier to understand, operationally faster, and can now be reused for other .NET languages. Neat!

Approach

Pros

  • easier code to understand
  • multiple language support
  • faster

Cons

  • Squiggles are less precise
  • A subset of C# and VB.NET is representable using IOperation. Some operations may have to remain on SyntaxNode.

The change moves most of the logic that is now contained in the void Analyze(SyntaxNodeAnalysisContext) method to the callback registered during void Initialize(AnalysisContext), only invoking code to analyze when preconditions are met. This has the benefit of both simplifying to reduce issues identified in #90 as well as separating concerns about filtering and analyzing symbols.

Strategy

  1. Convert to IOperation where possible
  2. Where less precise squiggles are observed, improve analyzer messaging to guide user to correct action (and/or provide Code Fixes)
  3. Improve analyzer messaging #85
  4. Add back in capabilities for precision with benchmarks to track performance

Since IOperation does not represent the full syntax tree, there may be loss of precision in the squiggle shown when writing code. This effort may require driving changes upstream with the Roslyn team.

Proposal

This may be best understood through an example. We will use the existing Mock.As<T>() analyzer as an example.

Existing

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsShouldBeUsedOnlyForInterfaceAnalyzer : DiagnosticAnalyzer
{
    internal const string RuleId = "Moq1300";
    private const string Title = "Moq: Invalid As type parameter";
    private const string Message = "Mock.As() should take interfaces only";

    private static readonly MoqMethodDescriptorBase MoqAsMethodDescriptor = new MoqAsMethodDescriptor();

    private static readonly DiagnosticDescriptor Rule = new(
        RuleId,
        Title,
        Message,
        DiagnosticCategory.Moq,
        DiagnosticSeverity.Error,
        isEnabledByDefault: true,
        helpLinkUri: $"https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/{RuleId}.md");

    /// <inheritdoc />
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")]
    private static void Analyze(SyntaxNodeAnalysisContext context)
    {
        if (context.Node is not InvocationExpressionSyntax invocationExpression)
        {
            return;
        }

        if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccessSyntax)
        {
            return;
        }

        if (!MoqAsMethodDescriptor.IsMatch(context.SemanticModel, memberAccessSyntax, context.CancellationToken))
        {
            return;
        }

        if (!memberAccessSyntax.Name.TryGetGenericArguments(out SeparatedSyntaxList<TypeSyntax> typeArguments))
        {
            return;
        }

        if (typeArguments.Count != 1)
        {
            return;
        }

        TypeSyntax typeArgument = typeArguments[0];
        SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(typeArgument, context.CancellationToken);

        if (symbolInfo.Symbol is ITypeSymbol { TypeKind: not TypeKind.Interface })
        {
            context.ReportDiagnostic(Diagnostic.Create(Rule, typeArgument.GetLocation()));
        }
    }
}

Using IOperation

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsShouldBeUsedOnlyForInterfaceAnalyzer2 : DiagnosticAnalyzer
{
    internal const string RuleId = "Moq1300";
    private const string Title = "Moq: Invalid As type parameter";
    private const string Message = "Mock.As() should take interfaces only";

    private static readonly DiagnosticDescriptor Rule = new(
        RuleId,
        Title,
        Message,
        DiagnosticCategory.Moq,
        DiagnosticSeverity.Error,
        isEnabledByDefault: true,
        helpLinkUri: $"https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/{RuleId}.md");

    /// <inheritdoc />
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

    /// <inheritdoc />
    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

        context.RegisterCompilationStartAction(static context =>
        {
            ImmutableArray<INamedTypeSymbol> mockTypes = context.Compilation.GetTypesByMetadataNames([WellKnownTypeNames.MoqMock, WellKnownTypeNames.MoqMock1]);
            if (mockTypes.IsEmpty)
            {
                return;
            }
            ImmutableArray<IMethodSymbol> asMethods = mockTypes
                .SelectMany(mockType => mockType.GetMembers("As"))
                .OfType<IMethodSymbol>()
                .Where(method => method.IsGenericMethod)
                .ToImmutableArray();
            if (asMethods.IsEmpty)
            {
                return;
            }
            context.RegisterOperationAction(context => Analyze(context, asMethods), OperationKind.Invocation);
        });
    }

    private static void Analyze(OperationAnalysisContext context, ImmutableArray<IMethodSymbol> wellKnownAsMethods)
    {
        if (context.Operation is not IInvocationOperation invocationOperation)
        {
            return;
        }

        IMethodSymbol targetMethod = invocationOperation.TargetMethod;

        if (!wellKnownAsMethods.Any(asMethod => asMethod.Equals(targetMethod.OriginalDefinition, SymbolEqualityComparer.Default)))
        {
            return;
        }

        ImmutableArray<ITypeSymbol> typeArguments = targetMethod.TypeArguments;
        if (typeArguments.Length != 1)
        {
            return;
        }

        if (typeArguments[0] is ITypeSymbol { TypeKind: not TypeKind.Interface })
        {
            context.ReportDiagnostic(Diagnostic.Create(Rule, invocationOperation.Syntax.GetLocation()));
        }
    }
}

The benchmark shows an improvement between the old and new methods.

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22635.3790)
Intel Core i9-10940X CPU 3.30GHz, 1 CPU, 28 logical and 14 physical cores
.NET SDK 8.0.301
  [Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX-512F+CD+BW+DQ+VL

Job=InProcess  Toolchain=InProcessEmitToolchain  InvocationCount=1
UnrollFactor=1
Method Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
OldMoq1300WithDiagnostics 181.6 ms 4.76 ms 13.58 ms 1.75 0.14 7000.0000 2000.0000 68.38 MB 1.39
NewMoq1300WithDiagnostics 159.9 ms 3.17 ms 8.00 ms 1.54 0.11 6000.0000 2000.0000 62.33 MB 1.26
Moq1300Baseline 104.3 ms 2.06 ms 5.02 ms 1.00 0.00 5000.0000 2000.0000 49.28 MB 1.00

Open Questions

This is a work in progress! We are patterning the initial design after Roslyn Analyzers.

The specific timing of the change is TBD. Pre-release packages will be submitted to NuGet.org to gather community feedback.

Validate `IsAnalyzerSuppressed` is needed

It feels like this code shouldn't be needed? I haven't gotten to the usage yet, but assuming we do need this, it feels like we should file + link a bug against Roslyn.
Originally posted by @MattKotsenas in #140 (comment)

The code is added to ensure the rule does not fire when it would otherwise be suppressed. Additional test cases / test harness work is required to support this programmatically.

This can be verified quickly with a prototype. If it does need to be there, an explainer why plus Matt's suggestion can be done.

Add analyzers for strict behavior mode

Given

public class TestClass
{
  public virtual Task TaskAsync() => Task.CompletedTask;
}

Two cases:

  1. Mock has no setup corresponding to the specified invocation
    When new Mock<TestClass>(MockBehavior.Strict).Object.TaskAsync(); executes it will throw an exception at runtime because MockBehavior.Strict is set but there is no Setup for the method TaskAsync
  2. Mock has no setup that provides a return value for the specified invocation
    Similar to the first case, if the setup does not include a return, there will be a runtime exception

Enable warnings as errors

  • Turn on warnings as errors
  • Also cleanup <NoWarn> elements
  • Clean up warnings backlog
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<MSBuildTreatWarningsAsErrors>true</MSBuildTreatWarningsAsErrors>

Moq1200 incorrectly firing on overridable member

Analyzer Moq1200 is incorrectly firing in the following case

public interface IFoo
{
  Task<bool> DoSomethingAsync();
}

var mock = new Mock<IFoo>();
mock.Setup(foo => foo.DoSomethingAsync().Result).Returns(true);

The analyzer indicates the Setup should be used only for overridable members; however, this member is on an interface while the error only makes sense if the method is on a class.

Reduce test cases by picking a value for MockBehavior

Some test cases have multiple MockBehavior values defined, expanding the number of test cases used.

This is

  1. Inconsistent between analyzer tests
  2. Does not change the analyzer behavior

That is, there is no difference between MockBehavior.Default, MockBehavior.Loose, and MockBehavior.Strict. There is a difference between not specifying a mock behavior and specifying a behavior. Tests should have one of each constructor

  1. Default (no args)
  2. Specifying a MockBehavior (e.g., MockBehavior.Default)

Where applicable (e.g., ConstructorArgumentsShouldMatchAnalyzer):

  1. Specifying constructor parameters
  2. Specifying a MockBehavior and also specifying constructor parameters

Set GitHub project defaults

  • Squash by default (appears to currently be set to merge)
  • Set the squashed commit message to the PR message

Add doppelganger test to all analyzers

NoConstructorArgumentsForInterfaceMockAnalyzerTests has a doppleganger test to ensure that it doesn't flag a user's class that happens to also be named Mock<T>. We should:

  1. Expand this scenario to cover all analyzers (probably with extended TestData?)
  2. Ensure we cover all the entrypoints to Moq

Moq1201 incorrectly firing on new Moq versions

public interface IFoo
{
  Task<bool> DoSomethingAsync();
}

var mock = new Mock<IFoo>();

// Preferred way 4.16+
mock.Setup(foo => foo.DoSomethingAsync().Result).Returns(true);

// Preferred way earlier than 4.16
mock.Setup(foo => foo.DoSomethingAsync()).ReturnsAsync(true);

From Moq Quickstart - Async Methods

There are several ways to set up "async" methods (e.g. methods returning a Task<T> or ValueTask<T>):

  • Starting with Moq 4.16, you can simply mock.Setup the returned task's .Result property. This works in nearly all setup and verification expressions:

    mock.Setup(foo => foo.DoSomethingAsync().Result).Returns(true);
  • In earlier versions, use setup helper methods like setup.ReturnsAsync, setup.ThrowsAsync where they are available:

    mock.Setup(foo => foo.DoSomethingAsync()).ReturnsAsync(true);

Clean up implicit usings

There are using statements that are covered by implicit usings that no longer need to be in files, and namespaces that should be added to reduce overall clutter.

Rule 1200 message not helpful when developer setup includes async result

When using the result of an async operation (returning generic Task<>), rule Moq1200 fires indicating setup should only be used for overridable types. This is technically true because Task<> is abstract, however, the message is not helpful / actionable by the developer.

Example

public class AsyncClient
{
    public virtual Task<string> GenericAsyncWithConcreteReturn() => Task.FromResult(string.Empty);
}

var mock = new Mock<AsyncClient>();

// Error 1200
mock.Setup(c => c.GenericAsyncWithConcreteReturn().Result);

// Expected 
mock.Setup(c => c.GenericAsyncWithConcreteReturn())
                .ReturnsAsync(string.Empty);

Propose adding a new rule, Moq1201, that is triggered when the setup method includes a Task that calls in to .Result. The error message reflects the guidance from Moq, which is to use the .ReturnsAsync method for setup.

Settle on style guide

  • Pick a style guide and how it should be
  • Set .editconfig to match
  • Remove legacy code formatting configuration

Upgrade (or at least test) with a newer version of Moq

We're currently testing with Moq 4.8.2, which is from 2018. v4.13.1 changed the internal implementation of .As<T>() (see devlooped/moq@b552aed) such that testing with that version or newer makes the AsAcceptOnlyInterfaces tests fail. Set a breakpoint on MoqMethodDescriptor.IsMoqMethod:

symbolInfo.Symbol.ToString()

Before: "Moq.Mock.As<AsAcceptOnlyInterface.TestBadAsForAbstractClass.BaseSampleClass>()"
After: "Moq.Mock<AsAcceptOnlyInterface.TestBadAsForAbstractClass.BaseSampleClass>.As<AsAcceptOnlyInterface.TestBadAsForAbstractClass.BaseSampleClass>()"

We should:

  1. Fix bug
  2. Consider testing against multiple versions of Moq
  3. Consider enforcing a minimum version?

Target netstandard2.0

The analyzer targets netstandard1.3, which is no longer supported. We should target netstandard2.0 for maximum compatibility.

Legacy TODO from README.md

  • Setup() should be us-ed for methods, not for properties
  • AdvancedMatcherAttribute should accept only IMatcher types
  • Checks for protected mocks when dynamically referencing their members
  • Setup() requires overridable members
  • Mock.Get() should not take literals

Add analyzer for default `MockBehavior`

Many devs have strong opinions on MockBehavior.Strict, MockBehavior.Loose and the default behavior (Loose).

We should find a way to balance letting devs enforce the rules they want while not enforcing a particular opinion.

Designs to consider

  1. An analyzer that flags when no behavior is specified or the default enum is used. This wouldn't enforce Loose vs Strict, just require people to use either Loose or Strict for clarity. I think this should be enabled by default as a warning.

  2. A rule that requires Strict mocking. This would flag Loose, Default, or no specification. This should either be off by default, or on at Info level and devs can change the default using .editorconfig or a .global config file.

Refactor tests to cover both global and namespace uses

It's unclear if this is a bug in the analyzer, bug in the test harness or something else.

However, the namespace shouldn't impact the analyzer. So once our tests are in a good spot, we should investigate.

Repro

"""
- namespace Moq.Analyzers.Test.DataAbstractClass.WithArgsPassed;
-
internal abstract class AbstractGenericClassWithCtor<T>
{

NuGet Package does not show up as an analyzer in VS Solution Explorer

The package that is available on nuget.org (and here on GitHub) does not show up in the Visual Studio Solution explorer under "Analyzers". I think the reason for this is that the package stores the dll in \lib\netstandard1.3 instead of \analyzers\dotnet\cs
NuGetPackageExplorer_Moq Analyzers_0 0 9

I tried to investigate this a bit, but both the Diagnostic.nuspec and Moq.Analyzers.csproj look fine. Both use "analyzers\dotnet\cs" as the path. I don't know which of these two is used to built the actual nuget, my guess is Diagnostic.nuspec as it's using 0.0.9 while the csproj is still on 0.0.6.

Could you please upload an updated nuget with the correct path?

Convert to explicit types

There is mixed use of implicit (var) and explicit types within the code base. Since we cannot use implicit types everywhere, and using implicit types makes things difficult during refactoring, switch to explicit types.

  • Update code to explicit types
  • Set code analysis rule to prevent use of var keyword

Review analyzer Description, Message, and Category

I'm not sure we're following best practices for the strings for analyzers.

  • Currently our Message doesn't accept any format strings, it probably belongs in Description instead
  • We should have a Message that includes the text of whatever's incorrect (via the {0} format strings)
  • All analyzers are currently in the "Moq" category; should these instead be in the "Usage" or another category?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.