Skip to content

Commit 282546d

Browse files
v1.8.0 (#26)
* Add the PosInfoMoq1003 and PosInfoMoq1004 rules implementation.
1 parent fc643f3 commit 282546d

18 files changed

Lines changed: 766 additions & 20 deletions

.github/workflows/github-actions-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
type: string
88
description: The version of the library
99
required: true
10-
default: 1.7.0
10+
default: 1.8.0
1111
VersionSuffix:
1212
type: string
1313
description: The version suffix of the library (for example rc.1)

PosInformatique.Moq.Analyzers.sln

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Design", "Design", "{815BE8
3131
docs\design\PosInfoMoq1000.md = docs\design\PosInfoMoq1000.md
3232
docs\design\PosInfoMoq1001.md = docs\design\PosInfoMoq1001.md
3333
docs\Design\PosInfoMoq1002.md = docs\Design\PosInfoMoq1002.md
34+
docs\Design\PosInfoMoq1003.md = docs\Design\PosInfoMoq1003.md
35+
docs\Design\PosInfoMoq1004.md = docs\Design\PosInfoMoq1004.md
3436
EndProjectSection
3537
EndProject
3638
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation", "{D9C84D36-7F9C-4EFB-BE6F-9F7A05FE957D}"

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Design rules used to make your unit tests more strongly strict.
2929
| [PosInfoMoq1000: `VerifyAll()` methods should be called when instantiate a `Mock<T>` instances](docs/Design/PosInfoMoq1000.md) | When instantiating a `Mock<T>` in the *Arrange* phase of an unit test, `VerifyAll()` method should be called in the *Assert* phase to check the setup methods has been called. |
3030
| [PosInfoMoq1001: The `Mock<T>` instance behavior should be defined to Strict mode](docs/Design/PosInfoMoq1001.md) | When instantiating a `Mock<T>` instance, the `MockBehavior` of the `Mock` instance should be defined to `Strict`. |
3131
| [PosInfoMoq1002: `Verify()` methods should be called when `Verifiable()` has been setup](docs/Design/PosInfoMoq1002.md) | When a mocked member has been setup with the `Verifiable()` method, the `Verify()` method must be called at the end of the unit test. |
32+
| [PosInfoMoq1003: The `Callback()` method should be used to check the parameters when mocking a method with `It.IsAny<T>()` arguments](docs/Design/PosInfoMoq1003.md) | When a mocked method contains a `It.IsAny<T>()` argument, the related parameter should be checked in the `Callback()` method. |
33+
| [PosInfoMoq1004: The `Callback()` parameter should not be ignored if it has been setup as an `It.IsAny<T>()` argument](docs/Design/PosInfoMoq1004.md) | When a mocked method contains a `It.IsAny<T>()` argument, the related parameter should not be ignored in the `Callback()` method. |
3234

3335
### Compilation
3436

docs/Design/PosInfoMoq1000.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ should be called in the *Assert* phase to check the setup methods has been calle
1919

2020
```csharp
2121
[Fact]
22-
public void GetCustomer_ShouldCallRepository()
22+
public void SendMail_ShouldCallSmtpService()
2323
{
2424
// Arrange
2525
var smtpService = new Mock<ISmtpService>();
2626
smtpService.Setup(s => s.SendMail("sender@domain.com", "Gilles"));
2727

28-
var service = new CustomerService(repository.Object);
28+
var service = new CustomerService(smtpService.Object);
2929

3030
// Act
3131
service.SendMail("Gilles");

docs/Design/PosInfoMoq1002.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ In the *Arrange* phase of an unit test, when `Verifiable()` method has been setu
1919

2020
```csharp
2121
[Fact]
22-
public void GetCustomer_ShouldCallRepository()
22+
public void SendMail_ShouldCallSmtpService()
2323
{
2424
// Arrange
2525
var smtpService = new Mock<ISmtpService>();
2626
smtpService.Setup(s => s.SendMail("sender@domain.com", "Gilles"))
2727
.Verifiable();
2828

29-
var service = new CustomerService(repository.Object);
29+
var service = new CustomerService(smtpService.Object);
3030

3131
// Act
3232
service.SendMail("Gilles");

docs/Design/PosInfoMoq1003.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# PosInfoMoq1003: The `Callback()` method should be used to check the parameters when mocking a method with `It.IsAny<T>()` arguments
2+
3+
| Property | Value |
4+
|-------------------------------------|---------------------------------------------------------------------------------------------------------------------|
5+
| **Rule ID** | PosInfoMoq1003 |
6+
| **Title** | The `Callback()` method should be used to check the parameters when mocking a method with `It.IsAny<T>()` arguments |
7+
| **Category** | Design |
8+
| **Default severity** | Warning |
9+
10+
## Cause
11+
12+
A method has been setup with `It.IsAny<T>()` arguments without checking the parameters in a `Callback()`
13+
method.
14+
15+
## Rule description
16+
17+
When setup a method using `It.IsAny<T>()` arguments, the parameters should be check in the `Callback()` method.
18+
19+
For example if we have the following code to test:
20+
21+
```csharp
22+
[Fact]
23+
public class CustomerService
24+
{
25+
private readonly ISmtpService smtpService;
26+
27+
public CustomerService(ISmtpService smtpService)
28+
{
29+
this.smtpService = smtpService;
30+
}
31+
32+
public void SendMail(string emailAddress)
33+
{
34+
this.smtpService.SendMail("sender@domain.com", emailAddress);
35+
}
36+
}
37+
```
38+
39+
If we mock the `ISmtpService.SendMail()` with a `It.IsAny<string>()` for the `emailAddress` argument,
40+
we can not check if the `CustomerService.SendMail()` has propagate correctly the value of the argument to the
41+
parameter of the `ISmtpService.SendMail()` method.
42+
43+
```csharp
44+
[Fact]
45+
public void SendMail_ShouldCallSmtpService()
46+
{
47+
var smtpService = new Mock<ISmtpService>();
48+
smtpService.Setup(s => s.SendMail("sender@domain.com", It.IsAny<string>())); // With It.IsAny<string>() we can not check that emailAddress has been correctly passed in the CustomerService.SendMail() method.
49+
50+
var service = new CustomerService(smtpService.Object);
51+
52+
service.SendMail("Gilles");
53+
}
54+
```
55+
56+
The `emailAddress` parameter passed to the `ISmtpService.SendMail()` method should be tested
57+
with the `Callback()` method, when mocking the `ISmtpService.SendMail()` method with a `It.IsAny<T>()` argument.
58+
59+
```csharp
60+
[Fact]
61+
public void SendMail_ShouldCallSmtpService()
62+
{
63+
var smtpService = new Mock<ISmtpService>();
64+
smtpService.Setup(s => s.SendMail("sender@domain.com", It.IsAny<string>())) // With It.IsAny() we should test the arguments if correctly propagated in the Callback() method.
65+
.Callback((string _, string emailAddress) =>
66+
{
67+
Assert.AreEqual("Gilles", em); // Check the emailAddress parameter.
68+
});
69+
70+
var service = new CustomerService(smtpService.Object);
71+
72+
service.SendMail("Gilles");
73+
}
74+
```
75+
76+
### Remarks
77+
- If the parameters of mocked methods are very simple (primitive values for example), pass directly the expected value in the arguments of the method. For example
78+
```csharp
79+
smtpService.Setup(s => s.SendMail("sender@domain.com", "Gilles"))
80+
```
81+
Instead of
82+
```csharp
83+
smtpService.Setup(s => s.SendMail("sender@domain.com", It.IsAny<string>()))
84+
```
85+
- Use the `Callback()` to assert complex parameters.
86+
87+
## How to fix violations
88+
89+
To fix a violation of this rule, use the `Callback()` method to check the `It.IsAny<T>()` arguments.
90+
91+
## When to suppress warnings
92+
93+
Do not suppress a warning from this rule. Normally `It.IsAny<T>()` arguments should be check and asserted in the `Callback()` methods.

docs/Design/PosInfoMoq1004.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# PosInfoMoq1004: The `Callback()` parameter should not be ignored if it has been setup as an `It.IsAny<T>()` argument.
2+
3+
| Property | Value |
4+
|-------------------------------------|------------------------------------------------------------------------------------------------------|
5+
| **Rule ID** | PosInfoMoq1004 |
6+
| **Title** | The `Callback()` parameter should not be ignored if it has been setup as an `It.IsAny<T>()` argument. |
7+
| **Category** | Design |
8+
| **Default severity** | Warning |
9+
10+
## Cause
11+
12+
A method has been setup with `It.IsAny<T>()` arguments and the parameter in the `Callback()` has not be used.
13+
14+
## Rule description
15+
16+
When setup a method using `It.IsAny<T>()` arguments, the parameters should be check in the `Callback()` method.
17+
18+
For example if we have the following code to test:
19+
20+
```csharp
21+
[Fact]
22+
public class CustomerService
23+
{
24+
private readonly ISmtpService smtpService;
25+
26+
public CustomerService(ISmtpService smtpService)
27+
{
28+
this.smtpService = smtpService;
29+
}
30+
31+
public void SendMail(string emailAddress)
32+
{
33+
this.smtpService.SendMail("sender@domain.com", emailAddress);
34+
}
35+
}
36+
```
37+
38+
If we mock the `ISmtpService.SendMail()` with a `It.IsAny<string>()` for the `emailAddress` argument,
39+
we can not check if the `CustomerService.SendMail()` has propagate correctly the value of the argument to the
40+
parameter of the `ISmtpService.SendMail()` method.
41+
42+
```csharp
43+
[Fact]
44+
public void SendMail_ShouldCallSmtpService()
45+
{
46+
var smtpService = new Mock<ISmtpService>();
47+
smtpService.Setup(s => s.SendMail("sender@domain.com", It.IsAny<string>()))
48+
.Callback((string _, string _) => { ... }); // The second parameter (emailAddress) should not be ignored and should be tested.
49+
50+
var service = new CustomerService(smtpService.Object);
51+
52+
service.SendMail("Gilles");
53+
}
54+
```
55+
56+
The `emailAddress` parameter passed to the `ISmtpService.SendMail()` method should be tested
57+
with the `Callback()` method, when mocking the `ISmtpService.SendMail()` method with a `It.IsAny<T>()` argument.
58+
59+
```csharp
60+
[Fact]
61+
public void SendMail_ShouldCallSmtpService()
62+
{
63+
var smtpService = new Mock<ISmtpService>();
64+
smtpService.Setup(s => s.SendMail("sender@domain.com", It.IsAny<string>())) // With It.IsAny() we should test the arguments if correctly propagated in the Callback() method.
65+
.Callback((string _, string emailAddress) =>
66+
{
67+
Assert.AreEqual("Gilles", em); // Check the emailAddress parameter.
68+
});
69+
70+
var service = new CustomerService(smtpService.Object);
71+
72+
service.SendMail("Gilles");
73+
}
74+
```
75+
76+
### Remarks
77+
- If the parameters of mocked methods are very simple (primitive values for example), pass directly the expected value in the arguments of the method. For example
78+
```csharp
79+
smtpService.Setup(s => s.SendMail("sender@domain.com", "Gilles"))
80+
```
81+
Instead of
82+
```csharp
83+
smtpService.Setup(s => s.SendMail("sender@domain.com", It.IsAny<string>()))
84+
```
85+
- Use the `Callback()` to assert complex parameters.
86+
87+
## How to fix violations
88+
89+
To fix a violation of this rule, use the `Callback()` method to check the `It.IsAny<T>()` arguments.
90+
91+
## When to suppress warnings
92+
93+
Do not suppress a warning from this rule. Normally `It.IsAny<T>()` arguments should be check and asserted in the `Callback()` methods.

src/Moq.Analyzers/AnalyzerReleases.Shipped.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
## Release 1.7.0
1+
## Release 1.8.0
2+
3+
### New Rules
4+
5+
Rule ID | Category | Severity | Notes
6+
--------|----------|----------|--------------------
7+
PosInfoMoq1003 | Design | Warning | The `Callback()` method should be used to check the parameters when mocking a method with `It.IsAny<T>()` arguments.
8+
PosInfoMoq1004 | Design | Warning | The `Callback()` parameter should not be ignored if it has been setup as an `It.IsAny<T>()` argument.
9+
10+
## Release 1.7.0
211

312
### New Rules
413

src/Moq.Analyzers/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzer.cs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
4646
return;
4747
}
4848

49-
// Try to determine if the invocation expression is a Callback() expression.
50-
var methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression, context.CancellationToken);
51-
52-
if (!moqSymbols.IsCallback(methodSymbol.Symbol))
53-
{
54-
return;
55-
}
56-
57-
// If yes, we extract the lambda expression of it.
49+
// Extract the lambda expression.
5850
var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel);
5951

6052
var callBackLambdaExpressionSymbol = moqExpressionAnalyzer.ExtractCallBackLambdaExpressionMethod(invocationExpression, out var lambdaExpression, context.CancellationToken);
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="CallBackDelegateParametersShouldNotBeIgnoredAnalyzer.cs" company="P.O.S Informatique">
3+
// Copyright (c) P.O.S Informatique. All rights reserved.
4+
// </copyright>
5+
//-----------------------------------------------------------------------
6+
7+
namespace PosInformatique.Moq.Analyzers
8+
{
9+
using System.Collections.Immutable;
10+
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.CSharp;
12+
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
using Microsoft.CodeAnalysis.Diagnostics;
14+
15+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
16+
public class CallBackDelegateParametersShouldNotBeIgnoredAnalyzer : DiagnosticAnalyzer
17+
{
18+
internal static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
19+
"PosInfoMoq1004",
20+
"The Callback() parameter should not be ignored if it has been setup as an It.IsAny<T>() argument",
21+
"The '{0}' parameter should not be ignored if it has been setup as an It.IsAny<T>() argument",
22+
"Design",
23+
DiagnosticSeverity.Warning,
24+
isEnabledByDefault: true,
25+
description: "The Callback() parameter should not be ignored if it has been setup as an It.IsAny<T>() argument.",
26+
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Design/PosInfoMoq1004.html");
27+
28+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
29+
30+
public override void Initialize(AnalysisContext context)
31+
{
32+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
33+
context.EnableConcurrentExecution();
34+
35+
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
36+
}
37+
38+
private static void Analyze(SyntaxNodeAnalysisContext context)
39+
{
40+
var invocationExpression = (InvocationExpressionSyntax)context.Node;
41+
42+
var moqSymbols = MoqSymbols.FromCompilation(context.Compilation);
43+
44+
if (moqSymbols is null)
45+
{
46+
return;
47+
}
48+
49+
var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel);
50+
51+
// Extracts the Setup() linked method.
52+
var setupMethod = moqExpressionAnalyzer.ExtractSetupMethod(invocationExpression, context.CancellationToken);
53+
54+
if (setupMethod is null)
55+
{
56+
return;
57+
}
58+
59+
// Check there is It.Any<T>() parameters.
60+
var itIsAnyArguments = new Dictionary<int, ChainInvocationArgument>();
61+
62+
for (var i = 0; i < setupMethod.InvocationArguments.Count; i++)
63+
{
64+
var argument = setupMethod.InvocationArguments[i];
65+
66+
if (moqSymbols.IsItIsAny(argument.Symbol))
67+
{
68+
// The Callback() method is required for the argument, add in the list.
69+
itIsAnyArguments.Add(i, argument);
70+
}
71+
}
72+
73+
if (itIsAnyArguments.Any())
74+
{
75+
// Here, it is mean Setup() method has been defined with some It.IsAny<T>() parameters.
76+
// Extracts the Callback() method (if exists).
77+
var callbackMethod = moqExpressionAnalyzer.ExtractCallBackLambdaExpressionMethod(invocationExpression, out var lambdaExpression, context.CancellationToken);
78+
79+
if (callbackMethod is null)
80+
{
81+
return;
82+
}
83+
84+
// Check each parameter of the Callback() method.
85+
for (var i = 0; i < callbackMethod.Parameters.Length; i++)
86+
{
87+
if (!itIsAnyArguments.TryGetValue(i, out var itIsAnyArgument))
88+
{
89+
// The parameter in the Callback() method is not related to a It.IsAny<T>() expression.
90+
continue;
91+
}
92+
93+
if (callbackMethod.Parameters[i].Name == "_")
94+
{
95+
// Raise warning for the parameter which is not used.
96+
var parameterName = setupMethod.InvocationArguments[i].ParameterSymbol.Name;
97+
98+
context.ReportDiagnostic(Rule, lambdaExpression!.ParameterList.Parameters[i].GetLocation(), parameterName);
99+
}
100+
}
101+
}
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)