Skip to content

Commit 2eb6f8d

Browse files
v2.0.0 (#54)
* Add the PosInfoMoq2017 to check Raise() and RaiseAsync() methods parameters (fixes #51). * Add the PosInfoMoq2018 to check the first arguments of the Raise() method (fixes #51). * Factorization some code in MoqSymbols. * Remove the usage of WithSpan() in the unit tests. * Add the PosInfoMoq1010 rule to check there is "+= null" when set the event to raise (fixes #52). * Check that the RaiseAsync() method is used with asynchronous events (fixes #51). * Change the version to 2.0.0 * Upgrade test libraries for Roselyn. * Add fixer for the PosInfoMoq1000 rule (fixes #53). * Add ConfigureAwait(false) for asynchronous method call. * Check the cancellation token propagation. * Check the method setup in SetupSequence() method (fixes: #33). * Improve the fixer PosInfoMoq1000 to add a new line for the first VerifyAll() if need.
1 parent ff5ecb1 commit 2eb6f8d

36 files changed

+1850
-272
lines changed

.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.13.0
10+
default: 2.0.0
1111
VersionSuffix:
1212
type: string
1313
description: The version suffix of the library (for example rc.1)

PosInformatique.Moq.Analyzers.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Design", "Design", "{815BE8
3838
docs\Design\PosInfoMoq1007.md = docs\Design\PosInfoMoq1007.md
3939
docs\Design\PosInfoMoq1008.md = docs\Design\PosInfoMoq1008.md
4040
docs\Design\PosInfoMoq1009.md = docs\Design\PosInfoMoq1009.md
41+
docs\Design\PosInfoMoq1010.md = docs\Design\PosInfoMoq1010.md
4142
EndProjectSection
4243
EndProject
4344
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation", "{D9C84D36-7F9C-4EFB-BE6F-9F7A05FE957D}"
@@ -59,6 +60,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation"
5960
docs\Compilation\PosInfoMoq2014.md = docs\Compilation\PosInfoMoq2014.md
6061
docs\Compilation\PosInfoMoq2015.md = docs\Compilation\PosInfoMoq2015.md
6162
docs\Compilation\PosInfoMoq2016.md = docs\Compilation\PosInfoMoq2016.md
63+
docs\Compilation\PosInfoMoq2017.md = docs\Compilation\PosInfoMoq2017.md
64+
docs\Compilation\PosInfoMoq2018.md = docs\Compilation\PosInfoMoq2018.md
65+
docs\Compilation\PosInfoMoq2019.md = docs\Compilation\PosInfoMoq2019.md
6266
EndProjectSection
6367
EndProject
6468
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Sandbox", "tests\Moq.Analyzers.Sandbox\Moq.Analyzers.Sandbox.csproj", "{07F970A1-1477-4D4C-B233-C9B4DA6E3AD6}"
@@ -71,6 +75,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
7175
.github\workflows\github-actions-release.yml = .github\workflows\github-actions-release.yml
7276
EndProjectSection
7377
EndProject
78+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
79+
ProjectSection(SolutionItems) = preProject
80+
src\.editorconfig = src\.editorconfig
81+
EndProjectSection
82+
EndProject
7483
Global
7584
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7685
Debug|Any CPU = Debug|Any CPU

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ Design rules used to make your unit tests more strongly strict.
4848
| [PosInfoMoq1006: The `It.IsAny<T>()` or `It.Is<T>()` arguments must match the parameters of the mocked method.](docs/Design/PosInfoMoq1006.md) | When setting up a method using `It.IsAny<T>()` or `It.Is<T>()` as arguments, the type `T` must exactly match the parameters of the configured method. |
4949
| [PosInfoMoq1007: The `Verify()` method must specify the `Times` argument.](docs/Design/PosInfoMoq1007.md) | When calling the `Verify()` method, if the `Times` argument is not specified, Moq will assume `Times.AtLeastOnce()` by default. |
5050
| [PosInfoMoq1008: The `Mock.Verify()` and `Mock.VerifyAll()` methods must specify at least one mock.](docs/Design/PosInfoMoq1008.md) | When calling the static methods `Mock.Verify()` or `Mock.VerifyAll()` without providing any `Mock<T>` instances, no verification is performed. |
51-
| [PosInfoMoq1009: Avoid using `Verifiable()` method](docs/Design/PosInfoMoq1008.md) | A `Verify()` of an `Mock<T>` instance has not been called in the *Assert* phase of an unit test for `Verifiable()` setups. |
51+
| [PosInfoMoq1009: Avoid using `Verifiable()` method](docs/Design/PosInfoMoq1009.md) | A `Verify()` of an `Mock<T>` instance has not been called in the *Assert* phase of an unit test for `Verifiable()` setups. |
52+
| [PosInfoMoq1010: Use `+= null` syntax when raising events with `Raise()`/`RaiseAsync()`](docs/Design/PosInfoMoq1010.md) | When using `Mock<T>.Raise()` or `Mock<T>.RaiseAsync()`, the lambda expression used to identify the event should consistently use the `+= null` syntax. |
5253

5354
### Compilation
5455

@@ -58,7 +59,7 @@ All the rules of this category should not be disabled (or changed their severity
5859
| Rule | Description |
5960
| - | - |
6061
| [PosInfoMoq2000: The `Returns()` or `ReturnsAsync()` methods must be call for Strict mocks](docs/Compilation/PosInfoMoq2000.md) | When a `Mock<T>` 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. |
61-
| [PosInfoMoq2001: The `Setup()`/`SetupSet()` method must be used only on overridable members](docs/Compilation/PosInfoMoq2001.md)) | The `Setup()` method must be applied only for overridable members. |
62+
| [PosInfoMoq2001: The `Setup()`/`SetupSequence()`/`SetupSet()` method must be used only on overridable members](docs/Compilation/PosInfoMoq2001.md)) | The `Setup()`/`SetupSequence()`/`SetupSet()` method must be applied only for overridable members. |
6263
| [PosInfoMoq2002: `Mock<T>` class can be used only to mock non-sealed class](docs/Compilation/PosInfoMoq2002.md) | The `Mock<T>` class can mock only interfaces or non-`sealed` classes. |
6364
| [PosInfoMoq2003: The `Callback()` delegate expression must match the signature of the mocked method](docs/Compilation/PosInfoMoq2003.md) | The delegate in the argument of the `Callback()` method must match the signature of the mocked method. |
6465
| [PosInfoMoq2004: Constructor arguments cannot be passed for interface mocks](docs/Compilation/PosInfoMoq2004.md) | No arguments can be passed to a mocked interface. |
@@ -74,6 +75,7 @@ All the rules of this category should not be disabled (or changed their severity
7475
| [PosInfoMoq2014: The `Callback()` delegate expression must not return a value.](docs/Compilation/PosInfoMoq2014.md) | The `Callback()` delegate expression must not return a value. |
7576
| [PosInfoMoq2015: The `Protected().Setup()` method must match the return type of the mocked method](docs/Compilation/PosInfoMoq2015.md) | The method setup with `Protected().Setup()` must match the return type of the mocked method. |
7677
| [PosInfoMoq2016: `Mock<T>` constructor with factory lambda expression can be used only with classes.](docs/Compilation/PosInfoMoq2016.md) | The factory lambda expression used in `Mock<T>` instantiation must used only for the classes. |
77-
78-
78+
| [PosInfoMoq2017: `Mock<T>.Raise()`/`RaiseAsync()` must use parameters matching the event signature.](docs/Compilation/PosInfoMoq2017.md) | The parameters passed to `Raise()` or `RaiseAsync()` must exactly match the parameters of the corresponding event delegate. |
79+
| [PosInfoMoq2018: The first parameter of `Raise()`/`RaiseAsync()` must be an event.](docs/Compilation/PosInfoMoq2018.md) | The first parameter passed to `Raise()` or `RaiseAsync()` must reference an **event** member of the mocked type. |
80+
| [PosInfoMoq2019: `RaiseAsync()` must be used only for events with async handlers (returning `Task`).](docs/Compilation/PosInfoMoq2019.md) | `Mock<T>.RaiseAsync()` must only be used with events whose delegate type returns `Task` (i.e., async events). |
7981

docs/Compilation/PosInfoMoq2001.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
# PosInfoMoq2001: The `Setup()`/`SetupSet()` method must be used only on overridable members
1+
# PosInfoMoq2001: The `Setup()`/`SetupSequence()`/`SetupSet()` method must be used only on overridable members
22

33
| Property | Value |
44
|-------------------------------------|---------------------------------------------------------------|
55
| **Rule ID** | PosInfoMoq2001 |
6-
| **Title** | The `Setup()`/`SetupSet()` method must be used only on overridable members |
6+
| **Title** | The `Setup()`/`SetupSequence()`/`SetupSet()` method must be used only on overridable members |
77
| **Category** | Compilation |
88
| **Default severity** | Error |
99

1010
## Cause
1111

12-
The `Setup()` or `SetupSet()` methods must be applied only for overridable members.
12+
The `Setup()`, `SetupSequence()` or `SetupSet()` methods must be applied only for overridable members.
1313
An overridable member is a **method** or **property** which is in:
1414
- An `interface`.
1515
- A non-`sealed` `class`. In this case, the member must be:
@@ -18,10 +18,10 @@ An overridable member is a **method** or **property** which is in:
1818

1919
## Rule description
2020

21-
The `Setup()` method must be applied only for overridable members.
21+
The `Setup()`, `SetupSequence()` and `SetupSet()` methods must be applied only for overridable members.
2222

2323
For example:
24-
- The following methods and properties can be mock and used in the `Setup()` method:
24+
- The following methods and properties can be mock and used in the `Setup()` and `SetupSequence()` method:
2525
- `IService.MethodCanBeMocked()`
2626
- `IService.PropertyCanBeMocked`
2727
- `Service.VirtualMethodCanBeMocked`
@@ -58,7 +58,7 @@ static methods which can not be overriden.
5858

5959
## How to fix violations
6060

61-
To fix a violation of this rule, be sure to mock a member in the `Setup()` or `SetupSet()` method which can be overriden.
61+
To fix a violation of this rule, be sure to mock a member in the `Setup()`, `SetupSequence()` or `SetupSet()` method which can be overriden.
6262

6363
## When to suppress warnings
6464

docs/Compilation/PosInfoMoq2017.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# PosInfoMoq2017: `Mock<T>.Raise()`/`RaiseAsync()` must use parameters matching the event signature
2+
3+
| Property | Value |
4+
|-----------------------|----------------------------------------------------------------------|
5+
| **Rule ID** | PosInfoMoq2017 |
6+
| **Title** | `Mock<T>.Raise()`/`RaiseAsync()` must use parameters matching the event signature |
7+
| **Category** | Compilation |
8+
| **Default severity** | Error |
9+
10+
## Cause
11+
12+
The parameters passed to `Raise()` or `RaiseAsync()` must exactly match the parameters of the corresponding event delegate.
13+
14+
## Rule description
15+
16+
When mocking events with **Moq**, calling `Mock<T>.Raise()` or `Mock<T>.RaiseAsync()` requires that the provided arguments match the event signature.
17+
18+
- With the **`Raise(event, EventArgs)`** overload:
19+
- Used for `EventHandler` or `EventHandler<T>`.
20+
- Moq automatically supplies the `sender` (the mocked object).
21+
- You only pass the `EventArgs` (or derived class) instance.
22+
23+
- With the **`Raise(event, params object[])`** overload:
24+
- You provide all the arguments of the event delegate yourself.
25+
- This includes the `sender` and all additional event arguments.
26+
27+
### Example
28+
29+
```csharp
30+
public class Service
31+
{
32+
public event EventHandler Changed;
33+
public event EventHandler<DataEventArgs> DataChanged;
34+
35+
public void DoSomething()
36+
{
37+
this.Changed?.Invoke(this, EventArgs.Empty);
38+
this.DataChanged?.Invoke(this, new DataEventArgs(42));
39+
}
40+
}
41+
42+
public class DataEventArgs : EventArgs
43+
{
44+
public int Value { get; }
45+
public DataEventArgs(int value) => Value = value;
46+
}
47+
```
48+
49+
#### Correct usage (matching parameters)
50+
51+
```csharp
52+
var serviceMock = new Mock<Service>();
53+
54+
// Raise with EventHandler (sender is mocked object, EventArgs required)
55+
serviceMock.Raise(s => s.Changed += null, EventArgs.Empty);
56+
57+
// Raise with EventHandler<T> (sender is mocked object, T required)
58+
serviceMock.Raise(s => s.DataChanged += null, new DataEventArgs(42));
59+
60+
// Raise with params object[] (sender + event args explicitly)
61+
serviceMock.Raise(s => s.DataChanged += null, "CustomSender", new DataEventArgs(42));
62+
```
63+
64+
#### Incorrect usage (parameters not matching)
65+
66+
```csharp
67+
var serviceMock = new Mock<Service>();
68+
69+
// Missing EventArgs
70+
serviceMock.Raise(s => s.Changed += null); //
71+
72+
// Wrong argument type
73+
serviceMock.Raise(s => s.DataChanged += null, EventArgs.Empty); //
74+
```
75+
76+
## How to fix violations
77+
78+
To fix a violation, ensure that the arguments passed to `Raise()` or `RaiseAsync()` match **exactly** the event delegate signature:
79+
80+
- `EventHandler`: `(object sender, EventArgs e)`
81+
- `EventHandler<T>`: `(object sender, T e)`
82+
83+
Depending on the overload:
84+
- With `Raise(event, EventArgs)` → provide only `EventArgs` or `T`.
85+
- With `Raise(event, params object[])` → provide both `sender` and `EventArgs` (or `T`).
86+
87+
## When to suppress warnings
88+
89+
Do not suppress errors from this rule. If skipped, Moq will throw a runtime exception such as `TargetParameterCountException`
90+
(with the following message *"Parameter count mismatch"*) or an `ArgumentException` to explain that a value can not be converted
91+
to an other type.

docs/Compilation/PosInfoMoq2018.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# PosInfoMoq2018: The first parameter of `Raise()`/`RaiseAsync()` must be an event
2+
3+
| Property | Value |
4+
|-----------------------|----------------------------------------------------------------------|
5+
| **Rule ID** | PosInfoMoq2018 |
6+
| **Title** | The first parameter of `Raise()`/`RaiseAsync()` must be an event |
7+
| **Category** | Compilation |
8+
| **Default severity** | Error |
9+
10+
## Cause
11+
12+
The first parameter passed to `Raise()` or `RaiseAsync()` must reference an **event** member of the mocked type.
13+
Using any other kind of member (property, method, field, etc.) is invalid.
14+
15+
## Rule description
16+
17+
When using Moq, `Mock<T>.Raise()` and `Mock<T>.RaiseAsync()` are designed to trigger **events** defined in the mocked type.
18+
If the provided lambda expression instead references something that is not an event (like a property assignment), the code is invalid.
19+
20+
### Example
21+
22+
```csharp
23+
public class Service
24+
{
25+
public event EventHandler Changed;
26+
public event EventHandler<DataEventArgs> DataChanged;
27+
public int Property { get; set; }
28+
}
29+
30+
public class DataEventArgs : EventArgs
31+
{
32+
public int Value { get; }
33+
public DataEventArgs(int value) => Value = value;
34+
}
35+
```
36+
37+
#### Correct usage (event targeted)
38+
39+
```csharp
40+
var serviceMock = new Mock<Service>();
41+
42+
// Raise() with EventHandler
43+
serviceMock.Raise(s => s.Changed += null, EventArgs.Empty);
44+
45+
// Raise() with EventHandler<T>
46+
serviceMock.Raise(s => s.DataChanged += null, new DataEventArgs(42));
47+
48+
// RaiseAsync() with EventHandler<T>
49+
await serviceMock.RaiseAsync(s => s.DataChanged += null, new DataEventArgs(42));
50+
```
51+
52+
#### Incorrect usage (invalid target)
53+
54+
```csharp
55+
var serviceMock = new Mock<Service>();
56+
57+
// Refers to a property, not an event
58+
serviceMock.Raise(s => s.Property = 10, EventArgs.Empty); //
59+
60+
// Same problem with RaiseAsync
61+
await serviceMock.RaiseAsync(s => s.Property = 10, EventArgs.Empty); //
62+
```
63+
64+
## How to fix violations
65+
66+
Ensure that the first parameter of `Raise()` and `RaiseAsync()` always references an **event** of the mocked type, never a property or field.
67+
68+
Correct:
69+
70+
```csharp
71+
serviceMock.Raise(s => s.Changed += null, EventArgs.Empty);
72+
```
73+
74+
Incorrect:
75+
76+
```csharp
77+
serviceMock.Raise(s => s.Property = 10, EventArgs.Empty);
78+
```
79+
80+
## When to suppress warnings
81+
82+
Do not suppress this warning. If bypassed, Moq itself will throw an `ArgumentException` exception at runtime with a message such as
83+
`Unsupported expression: m => (m.Property = 10)`.

docs/Compilation/PosInfoMoq2019.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# PosInfoMoq2019: `RaiseAsync()` must be used only for events with async handlers (returning `Task`)
2+
3+
| Property | Value |
4+
|-----------------------|----------------------------------------------------------------------|
5+
| **Rule ID** | PosInfoMoq2019 |
6+
| **Title** | `RaiseAsync()` must be used only for events with async handlers (returning `Task`) |
7+
| **Category** | Compilation |
8+
| **Default severity** | Error |
9+
10+
## Cause
11+
12+
`Mock<T>.RaiseAsync()` must only be used with events whose delegate type returns `Task` (i.e., async events).
13+
Using `RaiseAsync()` on events whose handlers return `void` (or any non-`Task` type) is invalid.
14+
15+
## Rule description
16+
17+
Moq provides `RaiseAsync()` to trigger asynchronous events. This requires that the event delegate returns `Task` (or `Task<T>`).
18+
If the event delegate returns `void` (like `EventHandler` or `EventHandler<T>`), `RaiseAsync()` is not appropriate and will cause runtime issues.
19+
20+
### Delegate examples
21+
22+
```csharp
23+
// Async event delegate
24+
public delegate Task AsyncEventHandler(object sender, EventArgs e);
25+
26+
// Classic sync delegates (return void)
27+
public delegate void VoidEventHandler(object sender, EventArgs e);
28+
public class DataEventArgs : EventArgs { public int Value { get; } public DataEventArgs(int v) => Value = v; }
29+
```
30+
31+
### Mocked type with both kinds of events
32+
33+
```csharp
34+
public class Service
35+
{
36+
public event AsyncEventHandler AsyncChanged; // returns Task
37+
public event EventHandler SyncChanged; // returns void
38+
public event EventHandler<DataEventArgs> SyncDataChanged; // returns void
39+
}
40+
```
41+
42+
### Correct usage (async delegate returning Task)
43+
44+
```csharp
45+
var serviceMock = new Mock<Service>();
46+
47+
// Use RaiseAsync with async event (Task-returning delegate)
48+
await serviceMock.RaiseAsync(s => s.AsyncChanged += null, EventArgs.Empty);
49+
```
50+
51+
### Incorrect usage (void-returning delegates)
52+
53+
```csharp
54+
var serviceMock = new Mock<Service>();
55+
56+
// EventHandler returns void — using RaiseAsync is invalid
57+
await serviceMock.RaiseAsync(s => s.SyncChanged += null, EventArgs.Empty); //
58+
59+
// EventHandler<T> returns void — using RaiseAsync is invalid
60+
await serviceMock.RaiseAsync(s => s.SyncDataChanged += null, new DataEventArgs(42)); //
61+
```
62+
63+
## How to fix violations
64+
65+
- If the event delegate returns `void`, use `Raise()` instead of `RaiseAsync()`.
66+
- If you need async semantics, change the event to use a `Task`-returning delegate, for example `AsyncEventHandler`.
67+
68+
### Fix examples
69+
70+
```csharp
71+
// Using Raise() for void-returning events
72+
serviceMock.Raise(s => s.SyncChanged += null, EventArgs.Empty);
73+
serviceMock.Raise(s => s.SyncDataChanged += null, new DataEventArgs(42));
74+
75+
// Using RaiseAsync() for Task-returning events
76+
await serviceMock.RaiseAsync(s => s.AsyncChanged += null, EventArgs.Empty);
77+
```
78+
79+
## When to suppress warnings
80+
81+
Do not suppress this rule.
82+
If disabled, Moq may throw a runtime exception (`NullReferenceException`) due to a known bug when `RaiseAsync()` is used on non-async events,
83+
instead of providing a clear error message.
84+
85+
Reference: https://github.com/devlooped/moq/issues/1568
41 KB
Loading

docs/Design/PosInfoMoq1000.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public void SendMail_ShouldCallSmtpService()
4040
To fix a violation of this rule, call the `VerifyAll()` in the *Assert* phase
4141
on the `Mock<T>` instances created during the *Arrange* phase.
4242

43+
### Visual Studio fixer
44+
A Visual Studio fixer exists to add the call to the `VerifyAll()` method at the end of the unit test in the current document, project or solution.
45+
![Visual Studio rule fixer](PosInfoMoq1000-Fixer.png)
46+
4347
## When to suppress warnings
4448

4549
Do not suppress a warning from this rule. Normally all setup methods must be call.

0 commit comments

Comments
 (0)