diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md new file mode 100644 index 00000000..775f221c --- /dev/null +++ b/CODE-OF-CONDUCT.md @@ -0,0 +1,6 @@ +# Code of Conduct + +This project has adopted the code of conduct defined by the Contributor Covenant +to clarify expected behavior in our community. + +For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..562e9329 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Contributing + +We welcome contributions. If you want to contribute to existing issues, check the +[help wanted](https://github.com/rjmurillo/moq.analyzers/labels/help%20wanted) or +[good first issue](https://github.com/rjmurillo/moq.analyzers/labels/good%20first%20issue) items in the backlog. + +If you have new ideas or want to complain about bugs, feel free to [create a new issue](https://github.com/rjmurillo/moq.analyzers/issues/new). diff --git a/README.md b/README.md index 3c96a942..366e898b 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,38 @@ # Moq.Analyzers -**Moq.Analyzers** is a Roslyn analyzer that helps to write unit tests using the popular and friend [Moq](https://github.com/devlooped/moq) library. Moq.Analyzers protects you from popular mistakes and warns you if something is wrong with your Moq configuration: +[![NuGet Version](https://img.shields.io/nuget/v/Moq.Analyzers?style=flat&logo=nuget&color=blue)](https://www.nuget.org/packages/Moq.Analyzers) +[![NuGet Downloads](https://img.shields.io/nuget/dt/Moq.Analyzers?style=flat&logo=nuget)](https://www.nuget.org/packages/Moq.Analyzers) +[![Main build](https://github.com/rjmurillo/moq.analyzers/actions/workflows/main.yml/badge.svg)](https://github.com/rjmurillo/moq.analyzers/actions/workflows/main.yml) -## Detected issues +**Moq.Analyzers** is a Roslyn analyzer that helps you to write unit tests using the popular +[Moq](https://github.com/devlooped/moq) framework. Moq.Analyzers protects you from common mistakes and warns you if +something is wrong with your Moq configuration. -* 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. +## Analyzer rules -## How to install +* 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 -Install ["Moq.Analyzers" NuGet package](https://www.nuget.org/packages/Moq.Analyzers) into test projects using Moq. +See [docs/rules](./docs/rules/README.md) for full documentation. -You must use an in-support version of the .NET SDK (i.e. 6+). +## Getting started -## Contributions are welcome! +Moq.Analyzers is installed from NuGet. Run this command for your test project(s): -Moq.Analyzers continues to evolve and add new features. Any help will be appreciated. You can report issues, develop new features, improve the documention, or do other cool stuff. +```powershell +dotnet add package Moq.Analyzers +``` -If you want to contribute to existing issues, check the [help wanted](https://github.com/rjmurillo/moq.analyzers/labels/help%20wanted) or [good first issue](https://github.com/rjmurillo/moq.analyzers/labels/good%20first%20issue) items in the backlog. If you have new ideas or want to complain about bugs, feel free to [create a new issue](https://github.com/rjmurillo/moq.analyzers/issues/new). +> NOTE: You must use a [supported version](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core) of +> the .NET SDK (i.e. 6.0 or later). -## Code of Conduct +## Contributions welcome -This project has adopted the code of conduct defined by the [Contributor Covenant](https://www.contributor-covenant.org/) to set expectations for behavior in our communication. For more information, see the [.NET Foundation's Contributor Convenant Code of Conduct](https://dotnetfoundation.org/about/policies/code-of-conduct) +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](./CONTRIBUTING.md). diff --git a/docs/rules/Moq1000.md b/docs/rules/Moq1000.md new file mode 100644 index 00000000..b7857ed0 --- /dev/null +++ b/docs/rules/Moq1000.md @@ -0,0 +1,30 @@ +# Moq1000: Sealed classes cannot be mocked + +| Item | Value | +| --- | --- | +| Enabled | True | +| Severity | Warning | +| CodeFix | False | +--- + +Mocking requires generating a subclass of the class to be mocked. Sealed classes cannot be subclassed. To fix: + +- Introduce an interface and mock that instead +- Use the real class and not a mock +- Unseal the class + +## Examples of patterns that are flagged by this analyzer + +```csharp +sealed class MyClass { } + +var mock = new Mock(); // Moq1000: Sealed classes cannot be mocked +``` + +## Solution + +```csharp +class MyClass { } + +var mock = new Mock(); +``` diff --git a/docs/rules/Moq1001.md b/docs/rules/Moq1001.md new file mode 100644 index 00000000..e9f7b6f6 --- /dev/null +++ b/docs/rules/Moq1001.md @@ -0,0 +1,35 @@ +# Moq1001: Mocked interfaces cannot have constructor parameters + +| Item | Value | +| --- | --- | +| Enabled | True | +| Severity | Warning | +| CodeFix | False | +--- + +Mocking interfaces requires generating a class on-the-fly that implements the interface. That generated class is +constructed using the default constructor. To fix: + +- Remove the constructor parameters + +## Examples of patterns that are flagged by this analyzer + +```csharp +interface IMyService +{ + void Do(string s); +} + +var mock = new Mock("123"); // Moq1001: Mocked interfaces cannot have constructor parameters +``` + +## Solution + +```csharp +interface IMyService +{ + void Do(string s); +} + +var mock = new Mock(); +``` diff --git a/docs/rules/Moq1002.md b/docs/rules/Moq1002.md new file mode 100644 index 00000000..42359ace --- /dev/null +++ b/docs/rules/Moq1002.md @@ -0,0 +1,34 @@ +# Moq1002: Parameters provided into mock do not match any existing constructors + +| Item | Value | +| --- | --- | +| Enabled | True | +| Severity | Warning | +| CodeFix | False | +--- + +In order to construct the mocked type, constructor parameters must match a constructor. To fix: + +- Match the arguments to `Mock` with a constructor of the mocked type + +## Examples of patterns that are flagged by this analyzer + +```csharp +class MyClass +{ + MyClass(string s) { } +} + +var mock = new Mock(3); // Moq1002: Parameters provided into mock do not match any existing constructors +``` + +## Solution + +```csharp +class MyClass +{ + MyClass(string s) { } +} + +var mock = new Mock("three"); +``` diff --git a/docs/rules/Moq1100.md b/docs/rules/Moq1100.md new file mode 100644 index 00000000..26ed98bf --- /dev/null +++ b/docs/rules/Moq1100.md @@ -0,0 +1,39 @@ +# Moq1100: Callback signature must match the signature of the mocked method + +| Item | Value | +| --- | --- | +| Enabled | True | +| Severity | Warning | +| CodeFix | True | +--- + +The signature of the `.Callback()` method must match the signature of the `.Setup()` method. To fix: + +- Ensure the parameters to `.Callback()` match the signature created by `.Setup()`. A code fix is available to automatically + match + +## Examples of patterns that are flagged by this analyzer + +```csharp +interface IMyService +{ + int Do(int i, string s, DateTime dt); +} + +var mock = new Mock() + .Setup(x => x.Do(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((string s1, int i1) => { }); // Moq1100: Callback signature must match the signature of the mocked method +``` + +## Solution + +```csharp +interface IMyService +{ + int Do(int i, string s, DateTime dt); +} + +var mock = new Mock() + .Setup(x => x.Do(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((int i, string s, DateTime dt) => { }); +``` diff --git a/docs/rules/Moq1101.md b/docs/rules/Moq1101.md new file mode 100644 index 00000000..1760b89d --- /dev/null +++ b/docs/rules/Moq1101.md @@ -0,0 +1,32 @@ +# Moq1101: SetupGet/SetupSet should be used for properties, not for methods + +| Item | Value | +| --- | --- | +| Enabled | True | +| Severity | Warning | +| CodeFix | False | +--- + +`.SetupGet()` and `.SetupSet()` are methods for mocking properties, not methods. Use `.Setup()` to mock methods instead. + +## Examples of patterns that are flagged by this analyzer + +```csharp +interface IMyInterface +{ + string Method(); +} + +var mock = new Mock().SetupGet(x => x.Method()); // Moq1101: SetupGet/SetupSet should be used for properties, not for methods +``` + +## Solution + +```csharp +interface IMyInterface +{ + string Method(); +} + +var mock = new Mock().Setup(x => x.Method()); +``` diff --git a/docs/rules/Moq1200.md b/docs/rules/Moq1200.md new file mode 100644 index 00000000..60d83132 --- /dev/null +++ b/docs/rules/Moq1200.md @@ -0,0 +1,36 @@ +# Moq1200: Setup should be used only for overridable members + +| Item | Value | +| --- | --- | +| Enabled | True | +| Severity | Error | +| CodeFix | False | +--- + +Mocking requires generating a subclass of the class to be mocked. Methods not marked `virtual` cannot be overridden. +To fix: + +- Mock an interface instead of a clas +- Make the method to be mocked `virtual` + +```csharp +class SampleClass +{ + int Property { get; set; } +} + +var mock = new Mock() + .Setup(x => x.Property); // Moq1200: Setup should be used only for overridable members +``` + +## Solution + +```csharp +class SampleClass +{ + virtual int Property { get; set; } +} + +var mock = new Mock() + .Setup(x => x.Property); +``` diff --git a/docs/rules/Moq1201.md b/docs/rules/Moq1201.md new file mode 100644 index 00000000..1f4e6250 --- /dev/null +++ b/docs/rules/Moq1201.md @@ -0,0 +1,35 @@ +# Moq1201: Setup of async methods should use `.ReturnsAsync` instance instead of `.Result` + +| Item | Value | +| --- | --- | +| Enabled | True | +| Severity | Error | +| CodeFix | False | +--- + +Moq now supports the `.ReturnsAsync()` method to support mocking async methods. Use it instead of returning `.Result`, +[which can cause issues](https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#avoid-using-taskresult-and-taskwait). + +## Examples of patterns that are flagged by this analyzer + +```csharp +class AsyncClient +{ + virtual Task GetAsync() => Task.FromResult(string.Empty); +} + +var mock = new Mock() + .Setup(c => c.GetAsync().Result); // Moq1201: Setup of async methods should use .ReturnsAsync instance instead of .Result +``` + +## Solution + +```csharp +class AsyncClient +{ + virtual Task GetAsync() => Task.FromResult(string.Empty); +} + +var mock = new Mock() + .Setup(c => c.GetAsync()).ReturnsAsync(string.Empty); +``` diff --git a/docs/rules/Moq1300.md b/docs/rules/Moq1300.md new file mode 100644 index 00000000..42de793b --- /dev/null +++ b/docs/rules/Moq1300.md @@ -0,0 +1,47 @@ +# Moq1300: `Mock.As()` should take interfaces only + +| Item | Value | +| --- | --- | +| Enabled | True | +| Severity | Error | +| CodeFix | False | +--- + +The `.As()` method is used when a mocked object must implement multiple interfaces. It cannot be used with abstract or +concrete classes. To fix: + +- Change the method to use an interface +- Remove the `.As()` method + +## Examples of patterns that are flagged by this analyzer + +```csharp +interface ISampleInterface +{ + int Calculate(int a, int b); +} + +class SampleClass +{ + int Calculate() => 0; +} + +var mock = new Mock() + .As(); // Moq1300: Mock.As() should take interfaces only +``` + +## Solution + +```csharp +interface ISampleInterface +{ + int Calculate(int a, int b); +} + +class SampleClass +{ + int Calculate() => 0; +} + +var mock = new Mock(); +``` diff --git a/docs/rules/README.md b/docs/rules/README.md new file mode 100644 index 00000000..e6be3865 --- /dev/null +++ b/docs/rules/README.md @@ -0,0 +1,12 @@ +# Diagnostics / rules + +| ID | Title | +| --- | --- | +[Moq1000](./Moq1000.md) | Sealed classes cannot be mocked +[Moq1001](./Moq1001.md) | Mocked interfaces cannot have constructor parameters +[Moq1002](./Moq1002.md) | Parameters provided into mock do not match any existing constructors +[Moq1100](./Moq1100.md) | Callback signature must match the signature of the mocked method +[Moq1101](./Moq1101.md) | SetupGet/SetupSet should be used for properties, not for methods +[Moq1200](./Moq1200.md) | Setup should be used only for overridable members +[Moq1201](./Moq1201.md) | Setup of async methods should use `.ReturnsAsync` instance instead of `.Result` +[Moq1300](./Moq1300.md) | `Mock.As()` should take interfaces only