From 14f68a6272a21e66819472f4bbbe007ddde9af59 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Tue, 7 Nov 2023 14:00:04 +0100 Subject: [PATCH] v1.1.0 (#2) * Add the MQ2000 rules to check that Returns() methods are called with Strict behavior mocks. * Change the DiagnosticId with the **PosInfoMoq** prefix to avoid conflicts with other Analyzers. --- PosInformatique.Moq.Analyzers.sln | 12 +- README.md | 14 +- build/azure-pipelines-release.yaml | 2 +- docs/Compilation/PosInfoMoq2000.md | 85 +++++ .../MQ1000.md => Design/PosInfoMoq1000.md} | 8 +- .../MQ1001.md => Design/PosInfoMoq1001.md} | 16 +- src/Moq.Analyzers/AnalyzerReleases.Shipped.md | 14 +- ...kInstanceShouldBeStrictBehaviorAnalyzer.cs | 68 +--- ...stReturnValueWithStrictBehaviorAnalyzer.cs | 105 ++++++ .../VerifyAllShouldBeCalledAnalyzer.cs | 16 +- .../SetBehaviorToStrictCodeFixProvider.cs | 20 +- src/Moq.Analyzers/MockExpressionHelper.cs | 165 ++++++++- src/Moq.Analyzers/Moq.Analyzers.csproj | 18 +- src/Moq.Analyzers/MoqSymbols.cs | 174 ++++++++++ ...tanceShouldBeStrictBehaviorAnalyzerTest.cs | 98 +----- ...turnValueWithStrictBehaviorAnalyzerTest.cs | 322 ++++++++++++++++++ .../VerifyAllShouldBeCalledAnalyzerTest.cs | 70 ++-- .../SetBehaviorToStrictCodeFixProviderTest.cs | 8 +- tests/Moq.Analyzers.Tests/MoqLibrary.cs | 51 +++ 19 files changed, 1038 insertions(+), 228 deletions(-) create mode 100644 docs/Compilation/PosInfoMoq2000.md rename docs/{design/MQ1000.md => Design/PosInfoMoq1000.md} (88%) rename docs/{design/MQ1001.md => Design/PosInfoMoq1001.md} (80%) create mode 100644 src/Moq.Analyzers/Analyzers/SetupMethodMustReturnValueWithStrictBehaviorAnalyzer.cs create mode 100644 src/Moq.Analyzers/MoqSymbols.cs create mode 100644 tests/Moq.Analyzers.Tests/Analyzers/SetupMethodMustReturnValueWithStrictBehaviorAnalyzerTest.cs create mode 100644 tests/Moq.Analyzers.Tests/MoqLibrary.cs diff --git a/PosInformatique.Moq.Analyzers.sln b/PosInformatique.Moq.Analyzers.sln index 73bb350..b723b4a 100644 --- a/PosInformatique.Moq.Analyzers.sln +++ b/PosInformatique.Moq.Analyzers.sln @@ -31,10 +31,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{DC2CDF77 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{3C20D95F-AB5F-44EC-8FB6-CB9827B7FD63}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "design", "design", "{815BE8D0-C7D5-4B4E-82E0-DE29C11F258E}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Design", "Design", "{815BE8D0-C7D5-4B4E-82E0-DE29C11F258E}" ProjectSection(SolutionItems) = preProject - docs\design\MQ1000.md = docs\design\MQ1000.md - docs\design\MQ1001.md = docs\design\MQ1001.md + docs\design\PosInfoMoq1001.md = docs\design\PosInfoMoq1001.md + docs\design\PosInfoMoq1000.md = docs\design\PosInfoMoq1000.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation", "{D9C84D36-7F9C-4EFB-BE6F-9F7A05FE957D}" + ProjectSection(SolutionItems) = preProject + docs\Compilation\PosInfoMoq2000.md = docs\Compilation\PosInfoMoq2000.md EndProjectSection EndProject Global @@ -60,6 +65,7 @@ Global {DC2CDF77-88A9-490D-84ED-34832943104A} = {1D59B801-B4D3-44FC-A2BE-F2F53AC54061} {3C20D95F-AB5F-44EC-8FB6-CB9827B7FD63} = {1D59B801-B4D3-44FC-A2BE-F2F53AC54061} {815BE8D0-C7D5-4B4E-82E0-DE29C11F258E} = {3C20D95F-AB5F-44EC-8FB6-CB9827B7FD63} + {D9C84D36-7F9C-4EFB-BE6F-9F7A05FE957D} = {3C20D95F-AB5F-44EC-8FB6-CB9827B7FD63} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3307E7F7-9CD7-4C12-B34A-943F5A8B62A4} diff --git a/README.md b/README.md index 590a7b7..ae7d788 100644 --- a/README.md +++ b/README.md @@ -26,5 +26,15 @@ Design rules used to make your unit tests more strongly strict. | Rule | Description | | - | - | -| [MQ1000: `Verify()` and `VerifyAll()` methods should be called when instantiate a `Mock` instances](docs/design/MQ1000.md) | When instantiating a `Mock` in the *Arrange* phase of an unit test, `Verify()` or `VerifyAll()` method should be called in the *Assert* phase to check the setup methods has been called. | -| [MQ1001: The `Mock` instance behavior should be defined to Strict mode](docs/design/MQ1001.md) | When instantiating a `Mock` instance, the `MockBehavior` of the `Mock` instance should be defined to `Strict`. | +| [PosInfoMoq1000: `Verify()` and `VerifyAll()` methods should be called when instantiate a `Mock` instances](docs/Design/PosInfoMoq1000.md) | When instantiating a `Mock` in the *Arrange* phase of an unit test, `Verify()` or `VerifyAll()` method should be called in the *Assert* phase to check the setup methods has been called. | +| [PosInfoMoq1001: The `Mock` instance behavior should be defined to Strict mode](docs/Design/PosInfoMoq1001.md) | When instantiating a `Mock` instance, the `MockBehavior` of the `Mock` instance should be defined to `Strict`. | + + +### Compilation + +Compilation rules check some error during the compilation to be sure that the execution of the unit tests with `Mock` will not raise exceptions. +All the rules of this category should not be disabled (or changed their severity differently of **Error**). + +| Rule | Description | +| - | - | +| [PosInfoMoq2000: The `Returns()` or `ReturnsAsync()` methods must be call for Strict mocks](docs/Compilation/PosInfoMoq2000.md) | When a `Mock` has been defined with the `Strict` behavior, the `Returns()` or `ReturnsAsync()` method must be called when setup a method to mock which returns a value. | diff --git a/build/azure-pipelines-release.yaml b/build/azure-pipelines-release.yaml index 86762a3..920b6c6 100644 --- a/build/azure-pipelines-release.yaml +++ b/build/azure-pipelines-release.yaml @@ -2,7 +2,7 @@ parameters: - name: VersionPrefix displayName: The version of the library type: string - default: 1.0.0 + default: 1.1.0 - name: VersionSuffix displayName: The version suffix of the library (rc.1). Use a space ' ' if no suffix. type: string diff --git a/docs/Compilation/PosInfoMoq2000.md b/docs/Compilation/PosInfoMoq2000.md new file mode 100644 index 0000000..4984c55 --- /dev/null +++ b/docs/Compilation/PosInfoMoq2000.md @@ -0,0 +1,85 @@ +# PosInfoMoq2000: The `Returns()` or `ReturnsAsync()` methods must be call for Strict mocks + +| Property | Value | +|-------------------------------------|--------------------------------------------------------------------------------------------| +| **Rule ID** | PosInfoMoq2000 | +| **Title** | The `Returns()` or `ReturnsAsync()` methods must be call for Strict mocks | +| **Category** | Compilation | +| **Default severity** | Error | + +## Cause + +A `Returns()` or `ReturnsAsync()` of an `Mock` instance with `Strict` behavior has not been called after a `Setup()` call. + +## Rule description + +When a `Mock` has been defined with the `Strict` behavior, the `Returns()` or `ReturnsAsync()` methods must be call +when setup a method to mock which returns a value. + +```csharp +[Fact] +public void GetCustomer_ShouldCallRepository() +{ + // Arrange + repository = new Mock(MockBehavior.Strict); + repository.Setup(r => r.GetData()); // No Returns() method has been specified. + ... + ... + // A MoqException will be thrown when the GetData() method will be called. +} +``` + +## How to fix violations + +To fix a violation of this rule, call the `Returns()` or `ReturnsAsync()` method after the `Setup()` +call to setup the method to mock. + +For example with the following code: + +```csharp +public interface IRepository +{ + int GetData(); +} + +public class CustomerService +{ + private readonly IRepository repository; + + public CustomerService(IRepository repository) + { + this.repository = repository; + } + + public int GetDataFromRepository() + { + return this.repository.GetData(); + } +} +``` + +For the associated unit test, the `Returns()` method have to be called for the `GetData()` method setup. + +```csharp +[Fact] +public void GetCustomer_ShouldCallRepository() +{ + // Arrange + repository = new Mock(MockBehavior.Strict); + repository.Setup(r => r.GetData()) + .Returns(1234); // The Returns() method is mandatory. + + var service = new CustomerService(repository.Object); + + // Act + var result = service.GetDataFromRepository(); + + // Arrange + result.Should().Be(1234); +} +``` + +## When to suppress warnings + +Do not suppress an error from this rule. If bypassed, the execution of the unit test will be failed with a `MoqException` +thrown with the *"Invocation needs to return a value and therefore must have a corresponding setup that provides it."* message. diff --git a/docs/design/MQ1000.md b/docs/Design/PosInfoMoq1000.md similarity index 88% rename from docs/design/MQ1000.md rename to docs/Design/PosInfoMoq1000.md index baf3117..dc2c448 100644 --- a/docs/design/MQ1000.md +++ b/docs/Design/PosInfoMoq1000.md @@ -1,8 +1,8 @@ -# MQ1000: `Verify()` and `VerifyAll()` methods should be called when instantiate a `Mock` instances +# PosInfoMoq1000: `Verify()` and `VerifyAll()` methods should be called when instantiate a `Mock` instances | Property | Value | |-------------------------------------|--------------------------------------------------------------------------------------------| -| **Rule ID** | MQ1000 | +| **Rule ID** | PosInfoMoq1000 | | **Title** | `Verify()` and `VerifyAll()` methods should be called when instantiate a Mock instances | | **Category** | Design | | **Default severity** | Warning | @@ -30,7 +30,7 @@ public void GetCustomer_ShouldCallRepository() // Act service.SendMail("Gilles"); - // Arrange + // Assert smtpService.VerifyAll(); // The VerifyAll() will check that the mocked ISmtpService.SendMail() has been called. } ``` @@ -42,4 +42,4 @@ on the `Mock` instances created during the *Arrange* phase. ## When to suppress warnings -Do not suppress a warning from this rule. Normally all setup methods must be called. +Do not suppress a warning from this rule. Normally all setup methods must be call. diff --git a/docs/design/MQ1001.md b/docs/Design/PosInfoMoq1001.md similarity index 80% rename from docs/design/MQ1001.md rename to docs/Design/PosInfoMoq1001.md index 78d5b9f..f7ef334 100644 --- a/docs/design/MQ1001.md +++ b/docs/Design/PosInfoMoq1001.md @@ -1,8 +1,8 @@ -# MQ1001: The `Mock` instance behavior should be defined to Strict mode +# PosInfoMoq1001: The `Mock` instance behavior should be defined to Strict mode | Property | Value | |-------------------------------------|------------------------------------------------------------------| -| **Rule ID** | MQ1001 | +| **Rule ID** | PosInfoMoq1001 | | **Title** | The `Mock` instance behavior should be defined to Strict mode | | **Category** | Design | | **Default severity** | Warning | @@ -24,11 +24,11 @@ public interface IRepository int GetData(); } -public class Service +public class CustomerService { private readonly IRepository repository; - public Service(IRepository repository) + public CustomerService(IRepository repository) { this.repository = repository; } @@ -48,9 +48,9 @@ the `GetData()` method has not been setup, the default `int` value (`0`) will be public void GetDataFromRepository() { // Arrange - var repository = new Mock(); // Default behavior (Loose) + var repository = new Mock(); // Default behavior (Loose) - // /!\ No methods on the ISmtpService has been setup ! + // /!\ No methods on the IRepository has been setup ! var service = new CustomerService(repository.Object); @@ -67,9 +67,9 @@ set the `MockBehavior` to `Strict` in the constructor of the `Mock` class. public void GetDataFromRepository() { // Arrange - var repository = new Mock(MockBehavior.Strict); // Strict behavior (Loose) + var repository = new Mock(MockBehavior.Strict); // Strict behavior (Loose) - // /!\ No methods on the ISmtpService has been setup ! + // /!\ No methods on the IRepository has been setup ! var service = new CustomerService(repository.Object); diff --git a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md index ee26229..d3ebfd5 100644 --- a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md @@ -1,8 +1,16 @@ -## Release 1.0.0 +## Release 1.1.0 ### New Rules Rule ID | Category | Severity | Notes --------|----------|----------|-------------------- -MQ1000 | Design | Warning | Verify() and VerifyAll() methods should be called when instantiate a Mock instances -MQ1001 | Design | Warning | The `Mock` instance behavior should be defined to Strict mode \ No newline at end of file +PosInfoMoq2000 | Compilation | Error | The `Returns()` or `ReturnsAsync()` method must be called for Strict mocks + +## Release 1.0.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +PosInfoMoq1000 | Design | Warning | `Verify()` and `VerifyAll()` methods should be called when instantiate a Mock instances +PosInfoMoq1001 | Design | Warning | The `Mock` instance behavior should be defined to Strict mode diff --git a/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs b/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs index ebda2ba..c6e3a7f 100644 --- a/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs @@ -15,7 +15,7 @@ namespace PosInformatique.Moq.Analyzers [DiagnosticAnalyzer(LanguageNames.CSharp)] public class MockInstanceShouldBeStrictBehaviorAnalyzer : DiagnosticAnalyzer { - public const string DiagnosticId = "MQ1001"; + public const string DiagnosticId = "PosInfoMoq1001"; private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( DiagnosticId, @@ -40,79 +40,23 @@ private static void Analyze(SyntaxNodeAnalysisContext context) { var objectCreation = (ObjectCreationExpressionSyntax)context.Node; - // Check there is "new Mock()" statement. - if (!MockExpressionHelper.IsMockCreation(objectCreation)) - { - return; - } + var moqSymbols = MoqSymbols.FromCompilation(context.Compilation); - // Check that the "new Mock()" statement have at least one argument (else Strict is missing...). - if (objectCreation.ArgumentList is null) + if (moqSymbols is null) { - var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); - context.ReportDiagnostic(diagnostic); - return; } - var firstArgument = objectCreation.ArgumentList.Arguments.FirstOrDefault(); - - if (firstArgument is null) + // Check there is "new Mock()" statement. + if (!MockExpressionHelper.IsMockCreation(moqSymbols, context.SemanticModel, objectCreation)) { - var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); - context.ReportDiagnostic(diagnostic); - return; } - // Gets the first argument of "new Mock(...)" and ensures it is a MemberAccessExpressionSyntax - // (because we searching for MockBehavior.Strict). - if (firstArgument.Expression is not MemberAccessExpressionSyntax memberAccessExpression) + if (!MockExpressionHelper.IsStrictBehavior(moqSymbols, context.SemanticModel, objectCreation)) { var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); context.ReportDiagnostic(diagnostic); - - return; - } - - // Find the "MockBehavior" type in the semantic model from Moq library. - var mockBehaviorType = context.Compilation.GetTypeByMetadataName("Moq.MockBehavior"); - - if (mockBehaviorType is null) - { - // Moq not installed (dependency of the Moq package missing), so we stop analysis. - return; - } - - // Check that the "memberAccessExpression.Expression" is applied on the Moq MockBehavior type. - var firstArgumentType = context.SemanticModel.GetSymbolInfo(memberAccessExpression.Expression); - - if (!SymbolEqualityComparer.Default.Equals(firstArgumentType.Symbol, mockBehaviorType)) - { - var diagnostic = Diagnostic.Create(Rule, firstArgument.GetLocation()); - context.ReportDiagnostic(diagnostic); - - return; - } - - // Find the Strict field in the semantic model - var strictField = mockBehaviorType.GetMembers("Strict").SingleOrDefault(); - - if (strictField is null) - { - // The field Strict seam to not exists (dependency of the Moq package missing ? Or something wrong ?), so we stop analysis. - return; - } - - // Check that the memberAccessExpression.Name reference the Strict field - var firstArgumentField = context.SemanticModel.GetSymbolInfo(memberAccessExpression.Name); - - if (!SymbolEqualityComparer.Default.Equals(firstArgumentField.Symbol, strictField)) - { - var diagnostic = Diagnostic.Create(Rule, memberAccessExpression.Name.GetLocation()); - context.ReportDiagnostic(diagnostic); - - return; } } } diff --git a/src/Moq.Analyzers/Analyzers/SetupMethodMustReturnValueWithStrictBehaviorAnalyzer.cs b/src/Moq.Analyzers/Analyzers/SetupMethodMustReturnValueWithStrictBehaviorAnalyzer.cs new file mode 100644 index 0000000..56efdde --- /dev/null +++ b/src/Moq.Analyzers/Analyzers/SetupMethodMustReturnValueWithStrictBehaviorAnalyzer.cs @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers +{ + using System.Collections.Immutable; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class SetupMethodMustReturnValueWithStrictBehaviorAnalyzer : DiagnosticAnalyzer + { + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + "PosInfoMoq2000", + "The Returns() or ReturnsAsync() methods must be call for Strict mocks", + "The Returns() or ReturnsAsync() methods must be call for Strict mocks", + "Compilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The Returns() or ReturnsAsync() methods must be call for Strict mocks."); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); + } + + private static void Analyze(SyntaxNodeAnalysisContext context) + { + var invocationExpression = (InvocationExpressionSyntax)context.Node; + + var moqSymbols = MoqSymbols.FromCompilation(context.Compilation); + + if (moqSymbols is null) + { + return; + } + + // Check is Setup() method. + if (!MockExpressionHelper.IsMockSetupMethod(moqSymbols, context.SemanticModel, invocationExpression, out var localVariableExpression)) + { + return; + } + + // Check the mocked method return type (if "void", we skip the analysis, because no Returns() is required). + var mockedMethodReturnTypeSymbol = MockExpressionHelper.GetSetupMethodReturnSymbol(moqSymbols, context.SemanticModel, invocationExpression); + if (mockedMethodReturnTypeSymbol is null) + { + return; + } + + if (mockedMethodReturnTypeSymbol.SpecialType == SpecialType.System_Void) + { + return; + } + + // Check the behavior of the mock instance is Strict. + if (!MockExpressionHelper.IsStrictBehavior(moqSymbols, context.SemanticModel, localVariableExpression!)) + { + return; + } + + // Check there Returns() method for the following calls (or Throws()). + var followingMethods = invocationExpression.Ancestors().OfType(); + + foreach (var followingMethod in followingMethods) + { + var methodSymbol = context.SemanticModel.GetSymbolInfo(followingMethod); + + if (moqSymbols.IsReturnsMethod(methodSymbol.Symbol)) + { + return; + } + + if (moqSymbols.IsReturnsAsyncMethod(methodSymbol.Symbol)) + { + return; + } + + if (moqSymbols.IsThrowsMethod(methodSymbol.Symbol)) + { + return; + } + + if (moqSymbols.IsThrowsAsyncMethod(methodSymbol.Symbol)) + { + return; + } + } + + // No returns method has been specified with Strict mode. Report the diagnostic issue. + var diagnostic = Diagnostic.Create(Rule, invocationExpression.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs b/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs index 6264082..ad4dd0c 100644 --- a/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs @@ -16,7 +16,7 @@ namespace PosInformatique.Moq.Analyzers public class VerifyAllShouldBeCalledAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( - "MQ1000", + "PosInfoMoq1000", "Verify() and VerifyAll() methods should be called when instantiate a Mock instances", "The Verify() or VerifyAll() method should be called at the end of the unit test", "Design", @@ -38,7 +38,14 @@ private static void Analyze(SyntaxNodeAnalysisContext context) { var objectCreation = (ObjectCreationExpressionSyntax)context.Node; - if (!MockExpressionHelper.IsMockCreation(objectCreation)) + var moqSymbols = MoqSymbols.FromCompilation(context.Compilation); + + if (moqSymbols is null) + { + return; + } + + if (!MockExpressionHelper.IsMockCreation(moqSymbols, context.SemanticModel, objectCreation)) { return; } @@ -54,6 +61,11 @@ private static void Analyze(SyntaxNodeAnalysisContext context) var variableNameModel = context.SemanticModel.GetDeclaredSymbol(variableName); + if (variableNameModel is null) + { + return; + } + // Check if there is a VerifyAll() invocation in the method's parent block. var parentMethod = objectCreation.Ancestors().OfType().FirstOrDefault(); diff --git a/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs index aaf91a8..9e487b1 100644 --- a/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs +++ b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs @@ -33,12 +33,24 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + { + return; + } + // Gets the location where is the issue in the code. var diagnostic = context.Diagnostics.First(); var diagnosticSpan = diagnostic.Location.SourceSpan; // Find the "ObjectCreationExpressionSyntax" in the parent of the location where is located the issue in the code. - var mockCreationExpression = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First(); + var parent = root.FindToken(diagnosticSpan.Start).Parent; + + if (parent is null) + { + return; + } + + var mockCreationExpression = parent.AncestorsAndSelf().OfType().First(); // Register a code to fix the enumeration. context.RegisterCodeFix( @@ -83,6 +95,12 @@ private static async Task AddMockBehiavorStrictArgumentAsync(Document SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments))); var oldRoot = await document.GetSyntaxRootAsync(cancellationToken); + + if (oldRoot is null) + { + return document; + } + var newRoot = oldRoot.ReplaceNode(oldMockCreationExpression, newMockCreationExpression); return document.WithSyntaxRoot(newRoot); diff --git a/src/Moq.Analyzers/MockExpressionHelper.cs b/src/Moq.Analyzers/MockExpressionHelper.cs index 4161bad..8634d29 100644 --- a/src/Moq.Analyzers/MockExpressionHelper.cs +++ b/src/Moq.Analyzers/MockExpressionHelper.cs @@ -6,23 +6,182 @@ namespace PosInformatique.Moq.Analyzers { + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; internal static class MockExpressionHelper { - public static bool IsMockCreation(ObjectCreationExpressionSyntax expression) + public static bool IsMockCreation(MoqSymbols moqSymbols, SemanticModel semanticModel, ObjectCreationExpressionSyntax expression) { - if (expression.Type is not GenericNameSyntax genericName) + var symbolInfo = semanticModel.GetSymbolInfo(expression.Type); + + if (!moqSymbols.IsMock(symbolInfo.Symbol)) + { + return false; + } + + return true; + } + + public static bool IsMockSetupMethod(MoqSymbols moqSymbols, SemanticModel semanticModel, InvocationExpressionSyntax invocationExpression, out IdentifierNameSyntax? localVariableExpression) + { + localVariableExpression = null; + + // Gets the member access expression "mock.XXXXX" + if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccessExpression) + { + return false; + } + + if (memberAccessExpression.Expression is not IdentifierNameSyntax lv) + { + return false; + } + + localVariableExpression = lv; + + var instanceVariable = semanticModel.GetSymbolInfo(memberAccessExpression.Expression); + + if (instanceVariable.Symbol is not ILocalSymbol instanceVariableSymbol) + { + return false; + } + + if (!moqSymbols.IsMock(instanceVariableSymbol.Type)) { return false; } - if (genericName.Identifier.ValueText != "Mock") + // Gets the method and check it is Setup() method. + var methodSymbolInfo = semanticModel.GetSymbolInfo(invocationExpression.Expression); + + if (!moqSymbols.IsSetupMethod(methodSymbolInfo.Symbol)) { return false; } return true; } + + public static bool IsStrictBehavior(MoqSymbols moqSymbols, SemanticModel semanticModel, ObjectCreationExpressionSyntax mockCreationExpression) + { + // Check that the "new Mock()" statement have at least one argument (else Strict is missing...). + if (mockCreationExpression.ArgumentList is null) + { + return false; + } + + var firstArgument = mockCreationExpression.ArgumentList.Arguments.FirstOrDefault(); + + if (firstArgument is null) + { + return false; + } + + // Gets the first argument of "new Mock(...)" and ensures it is a MemberAccessExpressionSyntax + // (because we searching for MockBehavior.Strict). + if (firstArgument.Expression is not MemberAccessExpressionSyntax memberAccessExpression) + { + return false; + } + + // Check that the "memberAccessExpression.Expression" is applied on the Moq MockBehavior type. + var firstArgumentType = semanticModel.GetSymbolInfo(memberAccessExpression.Expression); + + if (!moqSymbols.IsMockBehaviorEnum(firstArgumentType.Symbol)) + { + return false; + } + + // Check that the memberAccessExpression.Name reference the Strict field + var firstArgumentField = semanticModel.GetSymbolInfo(memberAccessExpression.Name); + + if (!moqSymbols.IsMockBehaviorStrictField(firstArgumentField.Symbol)) + { + return false; + } + + return true; + } + + public static bool IsStrictBehavior(MoqSymbols moqSymbols, SemanticModel semanticModel, IdentifierNameSyntax localVariableExpression) + { + // Go back to the parents nodes and iterate all the statements in the parents blocks to find + // the Mock instantiation (new Mock(...) to determines the mock behavior. + foreach (var block in localVariableExpression.Ancestors().OfType()) + { + var mockCreation = FindMockCreation(block, localVariableExpression.Identifier.ValueText); + + if (mockCreation is not null) + { + if (IsStrictBehavior(moqSymbols, semanticModel, mockCreation)) + { + return true; + } + } + } + + return false; + } + + public static ITypeSymbol? GetSetupMethodReturnSymbol(MoqSymbols moqSymbols, SemanticModel semanticModel, InvocationExpressionSyntax setupInvocationExpression) + { + if (setupInvocationExpression.ArgumentList is null) + { + return null; + } + + if (setupInvocationExpression.ArgumentList.Arguments.Count != 1) + { + return null; + } + + if (setupInvocationExpression.ArgumentList.Arguments[0].Expression is not SimpleLambdaExpressionSyntax lambdaExpression) + { + return null; + } + + if (lambdaExpression.Body is not InvocationExpressionSyntax methodExpression) + { + return null; + } + + var methodSymbolInfo = semanticModel.GetSymbolInfo(methodExpression); + + if (methodSymbolInfo.Symbol is not IMethodSymbol methodSymbol) + { + return null; + } + + return methodSymbol.ReturnType; + } + + private static ObjectCreationExpressionSyntax? FindMockCreation(BlockSyntax block, string variableName) + { + foreach (var statement in block.Statements.OfType()) + { + foreach (var variable in statement.Declaration.Variables) + { + if (variable.Identifier.Text != variableName) + { + continue; + } + + if (variable.Initializer is null) + { + continue; + } + + if (variable.Initializer.Value is not ObjectCreationExpressionSyntax objectCreationExpressionSyntax) + { + continue; + } + + return objectCreationExpressionSyntax; + } + } + + return null; + } } } diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index fc36000..8d2c6b4 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -1,11 +1,14 @@  - netstandard2.0 + netstandard2.0 true false true + latest + enable + true true @@ -14,10 +17,14 @@ https://github.com/PosInformatique/PosInformatique.Moq.Analyzers README.md + 1.1.0 + - Add new rules: + - PosInfoMoq2000: The `Returns()` or `ReturnsAsync()` methods must be call for Strict mocks. + 1.0.0 - Initial version with the followings rules: - - MQ1000: Verify() and VerifyAll() methods should be called when instantiate a Mock<T> instances - - MQ1001: The Mock<T> instance behavior should be defined to Strict mode. + - PosInfoMoq1000: Verify() and VerifyAll() methods should be called when instantiate a Mock<T> instances + - PosInfoMoq1001: The Mock<T> instance behavior should be defined to Strict mode. moq analyzers unittest c# roselyn compiler source code mock @@ -35,10 +42,7 @@ - - True - \ - + diff --git a/src/Moq.Analyzers/MoqSymbols.cs b/src/Moq.Analyzers/MoqSymbols.cs new file mode 100644 index 0000000..7d042d6 --- /dev/null +++ b/src/Moq.Analyzers/MoqSymbols.cs @@ -0,0 +1,174 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers +{ + using Microsoft.CodeAnalysis; + + internal sealed class MoqSymbols + { + private readonly INamedTypeSymbol mockBehaviorEnum; + + private readonly INamedTypeSymbol mockClass; + + private readonly IReadOnlyList setupMethods; + + private readonly ISymbol mockBehaviorStrictField; + + private MoqSymbols(INamedTypeSymbol mockClass, INamedTypeSymbol mockBehaviorEnum) + { + this.mockClass = mockClass; + this.mockBehaviorEnum = mockBehaviorEnum; + + this.setupMethods = mockClass.GetMembers("Setup").OfType().ToArray(); + this.mockBehaviorStrictField = mockBehaviorEnum.GetMembers("Strict").First(); + } + + public static MoqSymbols? FromCompilation(Compilation compilation) + { + var mockClass = compilation.GetTypeByMetadataName("Moq.Mock`1"); + + if (mockClass is null) + { + return null; + } + + var mockBehaviorEnum = compilation.GetTypeByMetadataName("Moq.MockBehavior"); + + if (mockBehaviorEnum is null) + { + return null; + } + + return new MoqSymbols(mockClass, mockBehaviorEnum); + } + + public bool IsMock(ISymbol? symbol) + { + if (symbol is null) + { + return false; + } + + if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.mockClass)) + { + return false; + } + + return true; + } + + public bool IsSetupMethod(ISymbol? symbol) + { + if (symbol is null) + { + return false; + } + + var originalDefinition = symbol.OriginalDefinition; + + foreach (var setupMethod in this.setupMethods) + { + if (SymbolEqualityComparer.Default.Equals(originalDefinition, setupMethod)) + { + return true; + } + } + + return false; + } + + public bool IsReturnsMethod(ISymbol? symbol) + { + if (symbol is null) + { + return false; + } + + if (symbol.Name != "Returns") + { + return false; + } + + return true; + } + + public bool IsReturnsAsyncMethod(ISymbol? symbol) + { + if (symbol is null) + { + return false; + } + + if (symbol.Name != "ReturnsAsync") + { + return false; + } + + return true; + } + + public bool IsThrowsMethod(ISymbol? symbol) + { + if (symbol is null) + { + return false; + } + + if (symbol.Name != "Throws") + { + return false; + } + + return true; + } + + public bool IsThrowsAsyncMethod(ISymbol? symbol) + { + if (symbol is null) + { + return false; + } + + if (symbol.Name != "ThrowsAsync") + { + return false; + } + + return true; + } + + public bool IsMockBehaviorEnum(ISymbol? symbol) + { + if (symbol is null) + { + return false; + } + + if (!SymbolEqualityComparer.Default.Equals(symbol, this.mockBehaviorEnum)) + { + return false; + } + + return true; + } + + public bool IsMockBehaviorStrictField(ISymbol? symbol) + { + if (symbol is null) + { + return false; + } + + if (!SymbolEqualityComparer.Default.Equals(symbol, this.mockBehaviorStrictField)) + { + return false; + } + + return true; + } + } +} diff --git a/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs index 9463f8e..8563c13 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs @@ -82,7 +82,7 @@ public class TestClass { public void TestMethod() { - var mock1 = new Mock(MockBehavior.[|Loose|]); + var mock1 = [|new Mock(MockBehavior.Loose)|]; } } @@ -124,14 +124,7 @@ public interface I { } } - - namespace Moq - { - public class Mock - { - public Mock() { } - } - }"; + " + MoqLibrary.Code; await Verify.VerifyAnalyzerAsync(source); } @@ -156,14 +149,7 @@ public interface I { } } - - namespace Moq - { - public class Mock - { - public Mock() { } - } - }"; + " + MoqLibrary.Code; await Verify.VerifyAnalyzerAsync(source); } @@ -203,7 +189,7 @@ public enum MockBehavior { Strict, Loose } } [Fact] - public async Task NoMoqNamespace() + public async Task NoMoqLibrary() { var source = @" namespace ConsoleApplication1 @@ -235,81 +221,5 @@ public enum MockBehavior { Strict, Loose } await Verify.VerifyAnalyzerAsync(source); } - - [Fact] - public async Task MockBehaviorTypeNotFromMoq() - { - var source = @" - namespace ConsoleApplication1 - { - using Moq; - using OtherNamespace; - - public class TestClass - { - public void TestMethod() - { - var mock1 = new Mock([|NotMockBehavior.Strict|]); - } - } - - public interface I - { - } - } - - namespace OtherNamespace - { - public enum NotMockBehavior { Strict, Loose } - } - - namespace Moq - { - using OtherNamespace; - - public class Mock - { - public Mock(NotMockBehavior _) { } - } - - public enum MockBehavior { Strict, Loose } - }"; - - await Verify.VerifyAnalyzerAsync(source); - } - - [Fact] - public async Task NoBehaviorStrict_InMoq() - { - var source = @" - namespace ConsoleApplication1 - { - using Moq; - - public class TestClass - { - public void TestMethod() - { - var mock1 = new Mock(MockBehavior.Loose); - } - } - - public interface I - { - } - } - - namespace Moq - { - public class Mock - { - public Mock(MockBehavior _) { } - } - - public enum MockBehavior { Loose } - }"; - - await Verify.VerifyAnalyzerAsync(source); - } } } \ No newline at end of file diff --git a/tests/Moq.Analyzers.Tests/Analyzers/SetupMethodMustReturnValueWithStrictBehaviorAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/SetupMethodMustReturnValueWithStrictBehaviorAnalyzerTest.cs new file mode 100644 index 0000000..83576cb --- /dev/null +++ b/tests/Moq.Analyzers.Tests/Analyzers/SetupMethodMustReturnValueWithStrictBehaviorAnalyzerTest.cs @@ -0,0 +1,322 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers.Tests +{ + using System.Threading.Tasks; + using Xunit; + using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + SetupMethodMustReturnValueWithStrictBehaviorAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + + public class SetupMethodMustReturnValueWithStrictBehaviorAnalyzerTest + { + [Fact] + public async Task Returns_NoDiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + mock1.Setup(i => i.TestMethod()) + .Callback() + .Callback() + .Property + .Returns(); + + var mock2 = new Mock(MockBehavior.Strict); + mock2.Setup(i => i.TestMethod()) + .Callback() + .Callback() + .Property + .ReturnsAsync(); + + var mock3 = new Mock(MockBehavior.Strict); + + // This scenario is not supported because the declaration is separated of the initialization. + // So here, no error should be raised (Missing Returns() with Strict behavior). + Mock mock4; + mock4 = new Mock(MockBehavior.Strict); + mock4.Setup(i => i.TestMethod()) + .Callback(); + + // This scenario is not supported because the declaration is separated of the initialization. + // So here, no error should be raised (Missing Returns() with Strict behavior). + Mock mock5 = mock1; + mock5.Setup(i => i.TestMethod()) + .Callback(); + + Mock mock6 = new Mock(MockBehavior.Strict); + mock6.Setup(i => i.TestMethod()) + .Callback() + .Throws(); + + Mock mock7 = new Mock(MockBehavior.Strict); + mock7.Setup(i => i.TestMethod()) + .Callback() + .ThrowsAsync(); + + var obj = new object(); // Ignored because not a Mock + obj.ToString(); + + var action = new Action(() => { }); + action(); // InvocationExpression ignored + } + } + + public interface I + { + int TestMethod(); + } + } + " + MoqLibrary.Code; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoReturns_WithBlocksAndMixedBeahviors_DiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock = new Mock(); + + var mock1 = new Mock(MockBehavior.Strict); + var mock2 = new Mock(MockBehavior.Strict); + { + { + [|mock1.Setup(i => i.TestMethod())|] + .Callback() + .Callback(); + } + } + + [|mock2.Setup(i => i.TestMethod())|] + .Callback() + .Callback(); + } + } + + public interface I + { + int TestMethod(); + } + } + " + MoqLibrary.Code; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoReturns_StrictMode_DiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + [|mock1.Setup(i => i.TestMethod())|] + .Callback() + .Callback(); + } + } + + public interface I + { + int TestMethod(); + } + } + " + + MoqLibrary.Code; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Theory] + [InlineData("MockBehavior.Loose")] + [InlineData("")] + [InlineData("1, 2")] + [InlineData("OtherEnum.A, 1, 2")] + [InlineData("MockBehavior.Loose, 1, 2")] + public async Task NoReturns_OtherMode_NoDiagnosticReported(string mockArguments) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(" + mockArguments + @"); + mock1.Setup(i => i.TestMethod()) + .Callback() + .Callback(); + } + } + + public interface I + { + int TestMethod(); + } + + public enum OtherEnum { A, B } + } + " + MoqLibrary.Code; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Theory] + [InlineData("MockBehavior.Strict")] + [InlineData("MockBehavior.Strict, 1, 2")] + [InlineData("MockBehavior.Loose")] + [InlineData("MockBehavior.Loose, 1, 2")] + [InlineData("")] + public async Task NoReturns_MockSetupMethodVoid_NoDiagnosticReported(string mockArguments) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(" + mockArguments + @"); + mock1.Setup(i => i.TestMethod()) + .Callback() + .Callback(); + } + } + + public interface I + { + void TestMethod(); + } + } + " + MoqLibrary.Code; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Theory] + [InlineData("MockBehavior.Strict")] + [InlineData("MockBehavior.Strict, 1, 2")] + [InlineData("MockBehavior.Loose")] + [InlineData("MockBehavior.Loose, 1, 2")] + [InlineData("")] + public async Task NoReturns_MockSetupProperty_NoDiagnosticReported(string mockArguments) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(" + mockArguments + @"); + mock1.Setup(i => i.Property = 10) + .Callback() + .Callback(); + } + } + + public interface I + { + int Property { get; set; } + } + } + " + MoqLibrary.Code; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoSetupMethod() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Verify(); + } + } + + public interface I + { + } + } + " + MoqLibrary.Code; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoMoqLibrary() + { + var source = @" + namespace ConsoleApplication1 + { + using OtherNamespace; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + } + } + + public interface I + { + } + } + + namespace OtherNamespace + { + public class Mock + { + public Mock(MockBehavior _) { } + } + + public enum MockBehavior { Strict, Loose } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + } +} \ No newline at end of file diff --git a/tests/Moq.Analyzers.Tests/Analyzers/VerifyAllShouldBeCalledAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/VerifyAllShouldBeCalledAnalyzerTest.cs index 52f89d1..f7329e0 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/VerifyAllShouldBeCalledAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/VerifyAllShouldBeCalledAnalyzerTest.cs @@ -48,16 +48,7 @@ public interface I { } } - - namespace Moq - { - public class Mock - { - public void VerifyAll() { } - public void Verify(int a, int b) { } - public object Property { get; set; } - } - }"; + " + MoqLibrary.Code; await Verify.VerifyAnalyzerAsync(source); } @@ -78,8 +69,6 @@ public void TestMethod() var mock2 = [|new Mock()|]; new Mock(); // No variable (ignored) - - mock1.Property = 1234; // Property access are ignored. } } @@ -87,14 +76,7 @@ public interface I { } } - - namespace Moq - { - public class Mock - { - public object Property { get; set; } - } - }"; + " + MoqLibrary.Code; await Verify.VerifyAnalyzerAsync(source); } @@ -120,16 +102,7 @@ public interface I { } } - - namespace Moq - { - public class Mock - { - public Mock(params object[] args) - { - } - } - }"; + " + MoqLibrary.Code; await Verify.VerifyAnalyzerAsync(source); } @@ -148,7 +121,8 @@ public void TestMethod() o.ToString(); } } - }"; + } + " + MoqLibrary.Code; await Verify.VerifyAnalyzerAsync(source); } @@ -175,7 +149,8 @@ public interface I public class Generic { } - }"; + } + " + MoqLibrary.Code; await Verify.VerifyAnalyzerAsync(source); } @@ -197,13 +172,40 @@ public interface I { } } + " + MoqLibrary.Code; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoMoqLibrary() + { + var source = @" + namespace ConsoleApplication1 + { + using OtherNamespace; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + } + } + + public interface I + { + } + } - namespace Moq + namespace OtherNamespace { public class Mock { - public object Property { get; set; } + public Mock(MockBehavior _) { } } + + public enum MockBehavior { Strict, Loose } }"; await Verify.VerifyAnalyzerAsync(source); diff --git a/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs index 7ab07b9..12e89fc 100644 --- a/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs +++ b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs @@ -27,7 +27,7 @@ public class TestClass { public void TestMethod() { - var mock = new Mock(MockBehavior.[|Loose|]); + var mock = [|new Mock(MockBehavior.Loose)|]; } } @@ -212,9 +212,9 @@ public class TestClass public void TestMethod() { var mock1 = [|new Mock(1, 2, 3)|]; - var mock2 = new Mock(MockBehavior.[|Loose|], 1, 2, 3); - var mock3 = new Mock([|OtherEnum.A|], 1, 2, 3); - var mock4 = new Mock([|int.MaxValue|], 1, 2, 3); + var mock2 = [|new Mock(MockBehavior.Loose, 1, 2, 3)|]; + var mock3 = [|new Mock(OtherEnum.A, 1, 2, 3)|]; + var mock4 = [|new Mock(int.MaxValue, 1, 2, 3)|]; } } diff --git a/tests/Moq.Analyzers.Tests/MoqLibrary.cs b/tests/Moq.Analyzers.Tests/MoqLibrary.cs new file mode 100644 index 0000000..6aa80d7 --- /dev/null +++ b/tests/Moq.Analyzers.Tests/MoqLibrary.cs @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers.Tests +{ + public static class MoqLibrary + { + public const string Code = @" + namespace Moq + { + using System; + + public class Mock + { + public Mock(MockBehavior _ = MockBehavior.Loose, params object[] args) { } + + public Mock(params object[] args) { } + + public ISetup Setup(Action act) { return null; } + + public void VerifyAll() { } + + public void Verify() { } + + public void Verify(int a, int b) { } + + public object Property { get; set; } + } + + public enum MockBehavior { Strict, Loose } + + public interface ISetup + { + ISetup Callback(); + + ISetup Property { get; } + + ISetup Returns(); + + ISetup ReturnsAsync(); + + ISetup Throws(); + + ISetup ThrowsAsync(); + } + }"; + } +}