diff --git a/README.md b/README.md index 2d1c36d..1e1883b 100644 --- a/README.md +++ b/README.md @@ -1,249 +1,250 @@ # MiniMock -Mini mock offers a _minimalistic_ approach to mocking in .Net. It is designed to be simple to use and easy to understand. -It is not as feature rich as other mocking frameworks, but aims to solve __95%__ of the use cases. For the remaining __5%__ you should consider creating a custom mock. - -Mini mock is __extremely strict__ requiring you to specify all features you want to mock. This is by design to make sure you are aware of what you are mocking. -Unmocked features will throw an exception if used. - -## Simple example +MiniMock offers a _minimalistic_ approach to mocking in .NET. It is designed to be simple to use and easy to understand. It is not as feature-rich as other mocking frameworks but aims to solve __95%__ of the use cases. For the remaining __5%__, you should consider creating a custom mock. + +MiniMock is __extremely strict__, requiring you to specify all features you want to mock. This is by design to make sure you are aware of what you are mocking. Unmocked features will throw an exception if used. + +[View the documentation here](https://oswaldsql.github.io/MiniMock/) + +## Table of Contents +- [Simple Example](#simple-example) +- [Key Feature Summary](#key-feature-summary) +- [Limitations](#limitations) +- [Installation & Initialization](#installation--initialization) +- [Quality of Life Features](#quality-of-life-features) + - [Fluent Interface](#fluent-interface) + - [Simple Return Values](#simple-return-values) + - [Multiple Return Values](#multiple-return-values) + - [Intercept Method Calls](#intercept-method-calls) + - [Async Methods](#async-methods) + - [Strict Mocking](#strict-mocking) + - [Adding Indexers](#adding-indexers) + - [Raising Events](#raising-events) + - [Argument Matching](#argument-matching) + +## Simple Example ```csharp - public interface IVersionLibrary - { - bool DownloadExists(string version); - } +public interface IVersionLibrary +{ + bool DownloadExists(string version); +} - [Fact] - [Mock] - public void SimpleExample() - { - var library = Mock.IVersionLibrary(config => - config.DownloadExists(returns: true)); +[Fact] +[Mock] +public void SimpleExample() +{ + var library = Mock.IVersionLibrary(config => + config.DownloadExists(returns: true)); - var actual = library.DownloadExists("2.0.0.0"); + var actual = library.DownloadExists("2.0.0.0"); - Assert.True(actual); - } + Assert.True(actual); +} ``` -## Key feature summery +## Key Feature Summary -- Minimalistic api with fluent method chaining, documentation and full intellisence -- Mocking of interfaces, abstract classes and virtual methods (with limitations) -- Mocking of methods, properties, indexers and events +- Minimalistic API with fluent method chaining, documentation, and full IntelliSense +- Mocking of interfaces, abstract classes, and virtual methods (with limitations) +- Mocking of methods, properties, indexers, and events - Simple factory methods to initialize mocks -- Mocking of Async methods, overloads and generic methods +- Mocking of async methods, overloads, and generic methods - Ref and out parameters in methods supported - Generic interfaces supported ## Limitations - No validation of calls -- Only support C# (workarounds exist for VB.Net and F#) -- No support for Generic methods ([issue #8](https://github.com/oswaldsql/MiniMock/issues/8)) -- Ref return values as ref properties are not supported ([issue #5](https://github.com/oswaldsql/MiniMock/issues/5)) -- Partially mocking of classes +- Only supports C# (workarounds exist for VB.NET and F#) +- Ref return values and ref properties are not supported ([issue #5](https://github.com/oswaldsql/MiniMock/issues/5)) +- Partial mocking of classes - Base classes with constructors with parameters are not currently supported ([Issue #4](https://github.com/oswaldsql/MiniMock/issues/4)) - No support for static classes or methods ## Installation & Initialization -Reference nuget package in your test project +Reference the NuGet package in your test project: -```csharp +```sh dotnet add package MiniMock ``` -Specify which interface to mock by using the [Mock] attribute before your test or test class. +Specify which interface to mock by using the `[Mock]` attribute before your test or test class: ```csharp [Fact] -[Mock] -public void MyTest { +[Mock] // Specify which interface to mock +public void MyTest() { + var mockRepo = Mock.IMyRepository(config => config // Create a mock using the mock factory + .CreateCustomerAsync(return: Guid.NewGuid()) // Configure your mock to your needs + ); + var sut = new CustomerMaintenance(mockRepo); // Inject the mock into your system under test + + sut.Create(customerDTO, cancellationToken); } ``` -Create a mock by using the mock factory - -```csharp -var mockRepository = Mock.IMyRepository(); -``` - -Configure your mock to your needs +## Quality of Life Features -```csharp -var mockRepo = Mock.IMyRepository(config => config.CreateCustormerAsync(return: Guid.NewGuid()); -``` - -Use the mocked object - -```csharp -var sut = new CustomerMaitinance(mockRepo); -sut.Create(customerDTO, cancelationToken); -``` - -## Quality of life features - -### Fluent interface with full intellisence and documentation. +### Fluent Interface -All mockable members are available through a _fluent interface_ with _intellisence_, _type safety_ and _documentation_. +All mockable members are available through a _fluent interface_ with _IntelliSense_, _type safety_, and _documentation_. -Since the mock code is generated at development time you can _inspect_, _stepped into_ and _debug_ the code. This also allows for _security_ and _vulnerability scanning_ of the code. +Since the mock code is generated at development time, you can _inspect_, _step into_, and _debug_ the code. This also allows for _security_ and _vulnerability scanning_ of the code. All code required to run MiniMock is generated and has _no runtime dependencies_. -### Simple return values +### Simple Return Values Simply specify what you expect returned from methods or properties. All parameters are ignored. ```csharp - var mockLibrary = Mock.IVersionLibrary(config => config - .DownloadExists(returns: true) // Returns true for any parameter - .DownloadLinkAsync(returns: new Uri("http://downloads/2.0.0")) // Returns a task with a download link - .CurrentVersion(value: new Version(2, 0, 0, 0)) // Sets the initial version to 2.0.0.0 - .Indexer(values: versions) // Provides a dictionary to retrieve and store versions - ); +var mockLibrary = Mock.IVersionLibrary(config => config + .DownloadExists(returns: true) // Returns true for any parameter + .DownloadLinkAsync(returns: new Uri("http://downloads/2.0.0")) // Returns a task with a download link + .CurrentVersion(value: new Version(2, 0, 0, 0)) // Sets the initial version to 2.0.0.0 + .Indexer(values: versions) // Provides a dictionary to retrieve and store versions +); ``` -### Multiple return values +### Multiple Return Values -Specify multiple return values for a method or property. The first value is returned for the first call, the second for the second call and so on. +Specify multiple return values for a method or property. The first value is returned for the first call, the second for the second call, and so on. ```csharp - var mockLibrary = Mock.IVersionLibrary(config => config - .DownloadExists(returnValues: true, false, true) // Returns true, false, true for the first, second and third call - .DownloadLinkAsync(returnValues: [Task.FromResult(new Uri("http://downloads/2.0.0")), Task.FromResult(new Uri("http://downloads/2.0.1"))]) // Returns a task with a download link for the first and second call - .DownloadLinkAsync(returnValues: new Uri("http://downloads/2.0.0"), new Uri("http://downloads/2.0.1")) // Returns a task with a download link for the first and second call - ); +var mockLibrary = Mock.IVersionLibrary(config => config + .DownloadExists(returnValues: true, false, true) // Returns true, false, true for the first, second, and third call + .DownloadLinkAsync(returnValues: [Task.FromResult(new Uri("http://downloads/2.0.0")), Task.FromResult(new Uri("http://downloads/2.0.1"))]) // Returns a task with a download link for the first and second call + .DownloadLinkAsync(returnValues: new Uri("http://downloads/2.0.0"), new Uri("http://downloads/2.0.1")) // Returns a task with a download link for the first and second call +); ``` -### Intercept method calls +### Intercept Method Calls ```csharp - [Fact] - [Mock] - public async Task InterceptMethodCalls() - { - var currentVersionMock = new Version(2, 0, 0); - - var versionLibrary = Mock.IVersionLibrary(config => config - .DownloadExists(call: (string s) => s.StartsWith("2.0.0") ? true : false ) // Returns true for version 2.0.0.x base on a string parameter - .DownloadExists(call: (Version v) => v is { Major: 2, Minor: 0, Revision: 0 })// Returns true for version 2.0.0.x based on a version parameter - //or - .DownloadExists(call: LocalIntercept) // calls a local function - .DownloadExists(call: version => this.ExternalIntercept(version, true)) // calls function in class - - .DownloadLinkAsync(call: s => Task.FromResult(new Uri($"http://downloads/{s}"))) // Returns a task containing a download link for version 2.0.0.x otherwise a error link - .DownloadLinkAsync(call: s => new Uri($"http://downloads/{s}")) // Returns a task containing a download link for version 2.0.0.x otherwise a error link - - .CurrentVersion(get: () => currentVersionMock, set: version => currentVersionMock = version) // Overwrites the property getter and setter - .Indexer(get: s => new Version(2,0,0,0), set: (s, version) => {}) // Overwrites the indexer getter and setter - ); +[Fact] +[Mock] +public async Task InterceptMethodCalls() +{ + var currentVersionMock = new Version(2, 0, 0); + + var versionLibrary = Mock.IVersionLibrary(config => config + .DownloadExists(call: (string s) => s.StartsWith("2.0.0") ? true : false) // Returns true for version 2.0.0.x based on a string parameter + .DownloadExists(call: (Version v) => v is { Major: 2, Minor: 0, Revision: 0 }) // Returns true for version 2.0.0.x based on a version parameter + // or + .DownloadExists(call: LocalIntercept) // Calls a local function + .DownloadExists(call: version => this.ExternalIntercept(version, true)) // Calls function in class + + .DownloadLinkAsync(call: s => Task.FromResult(new Uri($"http://downloads/{s}"))) // Returns a task containing a download link for version 2.0.0.x otherwise an error link + .DownloadLinkAsync(call: s => new Uri($"http://downloads/{s}")) // Returns a task containing a download link for version 2.0.0.x otherwise an error link - return; + .CurrentVersion(get: () => currentVersionMock, set: version => currentVersionMock = version) // Overwrites the property getter and setter + .Indexer(get: s => new Version(2, 0, 0, 0), set: (s, version) => {}) // Overwrites the indexer getter and setter + ); + + return; - bool LocalIntercept(Version version) - { - return version is { Major: 2, Minor: 0, Revision: 0 }; - } + bool LocalIntercept(Version version) + { + return version is { Major: 2, Minor: 0, Revision: 0 }; } +} - private bool ExternalIntercept(string version, bool startsWith) => startsWith ? version.StartsWith("2.0.0") : version == "2.0.0"; +private bool ExternalIntercept(string version, bool startsWith) => startsWith ? version.StartsWith("2.0.0") : version == "2.0.0"; ``` -### Async methods +### Async Methods -Simply return what you expect from async methods either as a Task object or a simple value. +Simply return what you expect from async methods either as a `Task` object or a simple value. ```csharp - var versionLibrary = Mock.IVersionLibrary(config => config - .DownloadLinkAsync(returns: Task.FromResult(new Uri("http://downloads/2.0.0"))) // Returns a task containing a download link for all versions - .DownloadLinkAsync(call: s => Task.FromResult(new Uri($"http://downloads/{s}"))) // Returns a task containing a download link for version 2.0.0.x otherwise a error link - // or - .DownloadLinkAsync(returns: new Uri("http://downloads/2.0.0")) // Returns a task containing a download link for all versions - .DownloadLinkAsync(call: s => new Uri($"http://downloads/{s}")) // Returns a task containing a download link for version 2.0.0.x otherwise a error link - ); +var versionLibrary = Mock.IVersionLibrary(config => config + .DownloadLinkAsync(returns: Task.FromResult(new Uri("http://downloads/2.0.0"))) // Returns a task containing a download link for all versions + .DownloadLinkAsync(call: s => Task.FromResult(new Uri($"http://downloads/{s}"))) // Returns a task containing a download link for version 2.0.0.x otherwise an error link + // or + .DownloadLinkAsync(returns: new Uri("http://downloads/2.0.0")) // Returns a task containing a download link for all versions + .DownloadLinkAsync(call: s => new Uri($"http://downloads/{s}")) // Returns a task containing a download link for version 2.0.0.x otherwise an error link +); ``` -### Strict mocking +### Strict Mocking -Unmocked features will always throw InvalidOperationException. +Unmocked features will always throw `InvalidOperationException`. ```csharp - [Fact] - [Mock] - public void UnmockedFeaturesAlwaysThrowInvalidOperationException() - { - var versionLibrary = Mock.IVersionLibrary(); - - var propertyException = Assert.Throws(() => versionLibrary.CurrentVersion); - var methodException = Assert.Throws(() => versionLibrary.DownloadExists("2.0.0")); - var asyncException = Assert.ThrowsAsync(() => versionLibrary.DownloadLinkAsync("2.0.0")); - var indexerException = Assert.Throws(() => versionLibrary["2.0.0"]); - } +[Fact] +[Mock] +public void UnmockedFeaturesAlwaysThrowInvalidOperationException() +{ + var versionLibrary = Mock.IVersionLibrary(); + + var propertyException = Assert.Throws(() => versionLibrary.CurrentVersion); + var methodException = Assert.Throws(() => versionLibrary.DownloadExists("2.0.0")); + var asyncException = Assert.ThrowsAsync(() => versionLibrary.DownloadLinkAsync("2.0.0")); + var indexerException = Assert.Throws(() => versionLibrary["2.0.0"]); +} ``` -### Adding indexers +### Adding Indexers Mocking indexers is supported either by overloading the get and set methods or by providing a dictionary with expected values. ```csharp - [Fact] - [Mock] - public void Indexers() - { - var versions = new Dictionary() {{"current", new Version(2,0,0,0)}}; - - var versionLibrary = Mock.IVersionLibrary(config => config - .Indexer(get: s => new Version(2,0,0,0), set: (s, version) => {}) // Overwrites the indexer getter and setter - .Indexer(values: versions) // Provides a dictionary to retrieve and store versions - ); - - var preCurrent = versionLibrary["current"]; - versionLibrary["current"] = new Version(3, 0, 0, 0); - var postCurrent = versionLibrary["current"]; - Assert.NotEqual(preCurrent, postCurrent); - } +[Fact] +[Mock] +public void Indexers() +{ + var versions = new Dictionary() {{"current", new Version(2,0,0,0)}}; + + var versionLibrary = Mock.IVersionLibrary(config => config + .Indexer(get: s => new Version(2,0,0,0), set: (s, version) => {}) // Overwrites the indexer getter and setter + .Indexer(values: versions) // Provides a dictionary to retrieve and store versions + ); + + var preCurrent = versionLibrary["current"]; + versionLibrary["current"] = new Version(3, 0, 0, 0); + var postCurrent = versionLibrary["current"]; + Assert.NotEqual(preCurrent, postCurrent); +} ``` -### Raising events +### Raising Events Raise events using an event trigger. ```csharp - Action? triggerNewVersionAdded = null; - - var versionLibrary = Mock.IVersionLibrary(config => config - .NewVersionAdded(trigger: out triggerNewVersionAdded) // Provides a trigger for when a new version is added - ); - - triggerNewVersionAdded?.Invoke(new Version(2, 0, 0, 0)); +Action? triggerNewVersionAdded = null; + +var versionLibrary = Mock.IVersionLibrary(config => config + .NewVersionAdded(trigger: out triggerNewVersionAdded) // Provides a trigger for when a new version is added +); + +triggerNewVersionAdded?.Invoke(new Version(2, 0, 0, 0)); ``` -### Argument matching +### Argument Matching -MiniMock does not support argument matching using matchers like other mocking frameworks. -Instead, you can use the call parameter to match arguments using predicates or internal functions. +MiniMock does not support argument matching using matchers like other mocking frameworks. Instead, you can use the call parameter to match arguments using predicates or internal functions. ```csharp - var versionLibrary = Mock.IVersionLibrary(config => config - .DownloadExists(call: version => version is { Major: 2, Minor: 0 }) // Returns true for version 2.0.x based on a version parameter - ); +var versionLibrary = Mock.IVersionLibrary(config => config + .DownloadExists(call: version => version is { Major: 2, Minor: 0 }) // Returns true for version 2.0.x based on a version parameter +); ``` -__using internal functions__ +__Using Internal Functions__ ```csharp - var versionLibrary = Mock.IVersionLibrary(config => - { - bool downloadExists(Version version) => version switch { - { Major: 1, Minor: 0 } => true, // Returns true for version 1.0.x based on a version parameter - { Major: 2, Minor: 0, Revision: 0 } => true, // Returns true for version 2.0.0.0 based on a version parameter - { Major: 3, } => false, // Returns false for version 3.x based on a version parameter - _ => throw new ArgumentException() // Throws an exception for all other versions - }; - - config.DownloadExists(downloadExists); - }); +var versionLibrary = Mock.IVersionLibrary(config => +{ + bool downloadExists(Version version) => version switch { + { Major: 1, Minor: 0 } => true, // Returns true for version 1.0.x based on a version parameter + { Major: 2, Minor: 0, Revision: 0 } => true, // Returns true for version 2.0.0.0 based on a version parameter + { Major: 3, } => false, // Returns false for version 3.x based on a version parameter + _ => throw new ArgumentException() // Throws an exception for all other versions + }; + + config.DownloadExists(downloadExists); +}); ``` diff --git a/docs/ADR/README.md b/docs/ADR/README.md new file mode 100644 index 0000000..446b285 --- /dev/null +++ b/docs/ADR/README.md @@ -0,0 +1,29 @@ +# Architecture Decision Records (ADR) + +Architecture Decision Records (ADRs) are documents that capture important architectural decisions made during the development of a MiniMock. + +All the ADRs have been approved and are considered final decisions for the project. + +## General ADRs [TL;DR](general/TLDR.md) + +- [Do We __Really__ Need a New Mocking Framework?](general/DoWeNeedANewMockingFramework.md) - Deciding whether to build a new mocking framework. +- [Matching Target API in Mock API](general/MatchingTargetApi.md) - Ensures the mock API closely mirrors the target API. +- [How Strict Should MiniMock Be?](general/HowStrictShouldMiniMockBe.md) - Deciding how strict the framework should be. +- [No Built-in Assertion Feature](general/NoBuiltInAssertionFeature.md) - Users choose their preferred assertion framework. +- [No Dependencies to Shared Libraries](general/NoDependencies.md) - Avoid dependencies on shared libraries. +- [Documentation and Examples](general/DocumentationAndExamples.md) - Approach to documentation and examples for the framework. +- [Logging and Debugging](general/LoggingAndDebugging.md) - Approach to logging and debugging within the framework. + +## Feature Specific ADRs [TL;DR](feature/TLDR.md) + +- [Support for Classes and Interfaces](feature/SupportForClassesAndInterfaces.md) - How should classes and interfaces be supported. +- [Support for Constructors](feature/SupportForConstructors.md) - Decision on supporting constructors in the mocking framework. +- [Creating Mocks](feature/CreatingMocks.md) - Decision on how to create mocks in the framework. +- [Support for Methods](feature/SupportForMethods.md) - Decision on supporting methods in the mocking framework. +- [Support for Properties](feature/SupportForProperties.md) - Decision on supporting properties in the mocking framework. +- [Support for Events](feature/SupportForEvents.md) - Decision on supporting events in the mocking framework. +- [Support for Indexers](feature/SupportForIndexers.md) - Decision on supporting indexers in the mocking framework. + +## Unsupported Features + +- [Unsupported Features](Unsupported/UnsupportedFeatures.md) - Decision on which features not to support. diff --git a/docs/ADR/Unsupported/UnsupportedFeatures.md b/docs/ADR/Unsupported/UnsupportedFeatures.md new file mode 100644 index 0000000..507d0cc --- /dev/null +++ b/docs/ADR/Unsupported/UnsupportedFeatures.md @@ -0,0 +1,23 @@ +# Unsupported features + +## Context + +In order to keep true to how mocking works some features will not be supported. + +## Decision + +**Extension methods** are not supported due to their nature. Extension methods are static methods that are called as if they were instance methods of the extended type. + +**Sealed classes** are not supported since they can not be inherited from and such do not fit how the framework works. + +**Static members** and **Static classes** are not supported due to the way the framework works. + +## Consequences + +### Positive: + +- **No need for magic**: Mocking these features would require breaking the standard supported functionality. + +### Negative: + +- **No magic**: Some functionality can be harder to test but if you need to test this maby you have some other issues. diff --git a/docs/ADR/feature/CreatingMocks.md b/docs/ADR/feature/CreatingMocks.md new file mode 100644 index 0000000..2b0e3d5 --- /dev/null +++ b/docs/ADR/feature/CreatingMocks.md @@ -0,0 +1,27 @@ +# Decision on How to Create Mocks + +## Context + +In the MiniMock framework, there is a need to establish a standardized approach for creating mocks. A consistent and efficient method for creating mocks will enhance the usability and maintainability of the framework. + +## Decision + +Mocks will be created using a mock factory. The mock factory will provide a centralized and consistent way to create and configure mocks, ensuring that all mocks are created following the same process and standards. +The constructors of the mock object will remain accessible but should only be used for limited purposes. + +## Consequences + +### Positive: + +- **Consistency**: Ensures that all mocks are created in a consistent manner. +- **Centralization**: Provides a single point of control for mock creation, making it easier to manage and update. +- **Ease of Use**: Simplifies the process of creating mocks for developers. + +### Negative: + +- **Complexity**: Introduces an additional layer of abstraction, which may add some complexity to the framework. +- **Maintenance**: Requires ongoing maintenance to ensure the mock factory remains up-to-date with any changes to the framework. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/feature/SupportForClassesAndInterfaces.md b/docs/ADR/feature/SupportForClassesAndInterfaces.md new file mode 100644 index 0000000..1febb03 --- /dev/null +++ b/docs/ADR/feature/SupportForClassesAndInterfaces.md @@ -0,0 +1,28 @@ +# Decision on Supporting Classes and Interfaces + +## Context + +In the MiniMock framework, there is a need to determine the scope of support for mocking different types of members. +While interfaces are the primary focus due to their flexibility and common usage in dependency injection, there is also a need to support classes to cover a broader range of use cases. + +## Decision + +The MiniMock framework will primarily focus on supporting interfaces but will also include support for classes. +This approach ensures that the framework can be used in a wide variety of scenarios, providing flexibility and comprehensive mocking capabilities. + +## Consequences + +### Positive: + +- **Flexibility**: Supports a wide range of use cases by allowing both interfaces and classes to be mocked. +- **Comprehensive**: Provides a robust mocking solution that can handle various types of dependencies. +- **Usability**: Makes the framework more versatile and useful for developers. + +### Negative: + +- **Complexity**: Adding support for classes may introduce additional complexity in the framework. +- **Maintenance**: Requires ongoing maintenance to ensure both interfaces and classes are supported effectively. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/feature/SupportForConstructors.md b/docs/ADR/feature/SupportForConstructors.md new file mode 100644 index 0000000..4edb25f --- /dev/null +++ b/docs/ADR/feature/SupportForConstructors.md @@ -0,0 +1,35 @@ +# Decision on Supporting Constructors + +## Context + +In the MiniMock framework, there is a need to determine the scope of support for mocking constructors. + +## Decision + +All constructors with the supported access level should be accessible. If no constructor exists, a parameterless constructor is created. +A factory for each option should be created. + +If only internal or private constructors exist, the class is not generated and a warning is registered. + +Additionally, the framework should support the following: + +- **Parameterized Constructors**: Allow mocking of constructors with parameters, providing flexibility for more complex scenarios. +- **Constructor Overloads**: Support multiple constructors with different parameter lists. +- **Dependency Injection**: Enable mocking of constructors that use dependency injection, ensuring compatibility with modern design patterns. + +## Consequences + +### Positive: + +- **Simplicity**: Simplifies the initial implementation by focusing on parameterless constructors. +- **Flexibility**: Supporting parameterized constructors and overloads provides more flexibility for developers. +- **Compatibility**: Ensures compatibility with dependency injection, making the framework more versatile. + +### Negative: + +- **Complexity**: Adding support for parameterized constructors and overloads increases the complexity of the framework. +- **Maintenance**: Requires ongoing maintenance to ensure that constructor mocking remains robust and up-to-date. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/feature/SupportForDelegates.md b/docs/ADR/feature/SupportForDelegates.md new file mode 100644 index 0000000..d909a24 --- /dev/null +++ b/docs/ADR/feature/SupportForDelegates.md @@ -0,0 +1,3 @@ +# Support for Delegates (WIP) + +Work in progress. diff --git a/docs/ADR/feature/SupportForEvents.md b/docs/ADR/feature/SupportForEvents.md new file mode 100644 index 0000000..b91f4ea --- /dev/null +++ b/docs/ADR/feature/SupportForEvents.md @@ -0,0 +1,31 @@ +# Decision on Supporting Events + +## Context + +In the MiniMock framework, there is a need to determine the scope of support for mocking events. Events are a crucial part of the C# language, enabling the publisher-subscriber pattern. Supporting all types of events is essential to ensure the framework's flexibility and usability. + +## Decision + +The MiniMock framework will support mocking all types of events. This includes standard events, custom events, and events with different delegate types. This decision ensures that the framework can handle a wide range of scenarios involving event handling. + +Events must be mockable using the following parameters: + +- Raise : A method to raise the event, triggering all subscribed handlers. +- Trigger : A delegate as a out parameter to be used to trigger the event. +- Add/Remove : Delegates matching the event's add and remove signatures with functionality to be executed. + +## Consequences + +### Positive: + +- **Comprehensive**: Ensures that the framework can handle all types of events, providing flexibility and comprehensive mocking capabilities. +- **Usability**: Enhances the framework's usability by allowing developers to mock events in various scenarios. + +### Negative: + +- **Complexity**: Supporting all types of events adds complexity to the framework. +- **Maintenance**: Requires ongoing maintenance to ensure that event mocking remains robust and up-to-date. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/feature/SupportForIndexers.md b/docs/ADR/feature/SupportForIndexers.md new file mode 100644 index 0000000..6a1ca50 --- /dev/null +++ b/docs/ADR/feature/SupportForIndexers.md @@ -0,0 +1,32 @@ +# Decision on Supporting Indexers + +## Context + +In the MiniMock framework, there is a need to determine the scope of support for mocking indexers. Indexers allow objects to be indexed in a similar way to arrays, and supporting them is essential for a comprehensive mocking framework. + +## Decision + +The MiniMock framework will support mocking indexers. This includes both read-only and read-write indexers, ensuring that the framework can handle a wide range of scenarios. + +Indexers must be mockable using the following parameters: + +- Get/Set : Delegates matching the indexer's getter and setter signature with functionality to be executed. +- Values : A dictionary optionally containing values to be used as the indexers source. + +if none of the above parameters are provided, accessing the indexer must throw a InvalidOperationException with a message in the form "The indexer for '__[indexer type]__' in '__[mocked class]__' is not explicitly mocked.". + +## Consequences + +### Positive: + +- **Comprehensive**: Ensures that the framework can handle indexers in both classes and interfaces. +- **Flexibility**: Provides developers with the ability to mock different kinds of indexers, enhancing the framework's usability. + +### Negative: + +- **Complexity**: Supporting both read-only and read-write indexers adds complexity to the framework. +- **Maintenance**: Requires ongoing maintenance to ensure that indexer mocking remains robust and up-to-date. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/feature/SupportForMethods.md b/docs/ADR/feature/SupportForMethods.md new file mode 100644 index 0000000..e9639e6 --- /dev/null +++ b/docs/ADR/feature/SupportForMethods.md @@ -0,0 +1,57 @@ +# Decision on Supporting Methods + +## Context + +In the MiniMock framework, there is a need to determine the scope of support for mocking methods. Supporting all standard ways of creating methods is essential to ensure the framework's +flexibility and usability. + +The following type of methods must be supported +- __Asynchronous__ with Task<>, Task, CancellationToken +- __Overloaded__ methods in a way that keeps the required effort to setup the mock to a minimum. +- __Generic__ Including the 'where' __constraints__. +- __Out__ and __ref__ attributes on the method parameters. + +## Decision + +The MiniMock framework will support all standard ways of creating methods. This includes instance methods, static methods, virtual methods, and abstract methods. However, support for methods returning `ref` values will require additional work and will be addressed in future updates. See [issue #5](https://github.com/oswaldsql/MiniMock/issues/5) + +Methods must be mockable using the following parameters + +- Call : A Delegate matching the method signature with functionality to be executed. +- Throw : An exception to be thrown when the method is called. +- Return : A value to be returned when the method is called. +- ReturnValues : A sequence of values to be returned when the method is called multiple times. +- () : Methods returning `void` can be mocked using an empty delegate. + +if none of the above parameters are provided, calling the method must throw a InvalidOperationException with a message in the form "The method '__[method name]__' in '__[mocked class]__' is not explicitly mocked.". + +__Asynchronous__ methods are supported. Helper methods are provided to simplify the testing of asynchronous methods. Overloads of the following helper methods are added + +- Return : Allows for returning either a Task object or the object to be wrapped in the task object. +- ReturnValues : Allows for returning either a sequence of Task objects or a sequence of objects to be wrapped in task objects. +- () : Methods returning Task can also use the empty delegate. + +__Overloaded__ methods can either be mocked explicitly by using `Call` or collectively using the following + +- Throw : An exception to be thrown when calling any of the overwritten methods. +- Return : A value to be returned when a method with that return type is called. +- ReturnValues : A sequence of values to be returned when a method with those return types is called multiple times. +- () : Methods returning `void` or `Task` can be mocked using an empty delegate. + +Generic methods are supported. The generic type is passed as a type parameter to the 'call' labmda method. + +## Consequences + +### Positive: + +- **Comprehensive**: Ensures that the framework can handle a wide variety of method types. +- **Flexibility**: Provides developers with the ability to mock different kinds of methods, enhancing the framework's usability. + +### Negative: + +- **Complexity**: Supporting all standard methods adds complexity to the framework. +- **Ref Values**: Current issues with methods returning `ref` values need to be resolved, which may require significant effort. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/feature/SupportForProperties.md b/docs/ADR/feature/SupportForProperties.md new file mode 100644 index 0000000..4ea5c43 --- /dev/null +++ b/docs/ADR/feature/SupportForProperties.md @@ -0,0 +1,37 @@ +# Decision on Supporting Properties + +## Context + +In the MiniMock framework, there is a need to determine the scope of support for mocking properties. Properties are a fundamental part of C# classes and interfaces, and supporting them is essential for a comprehensive mocking framework. + +## Decision + +The MiniMock framework will support mocking both read-only and read-write properties. This includes properties in classes and interfaces, ensuring that the framework can handle a wide range of scenarios. + +Properties must be mockable using the following parameters: + +- Get/set : Delegates matching the property's getter and setter signature with functionality to be executed. +- Value : A value to be returned when the property is accessed. + +Get-only and set-only properties must only allow mocking of the corresponding getter or setter. + +if none of the above parameters are provided, Getting of setting the property must throw a InvalidOperationException with a message in the form "The property '__[property name]__' in '__[mocked class]__' is not explicitly mocked.". + + +## Consequences + +### Positive: + +- **Comprehensive**: Ensures that the framework can handle properties in both classes and interfaces. +- **Flexibility**: Provides developers with the ability to mock different kinds of properties, enhancing the framework's usability. + +### Negative: + +- **Complexity**: Supporting both read-only and read-write properties adds complexity to the framework. +- **Maintenance**: Requires ongoing maintenance to ensure that property mocking remains robust and up-to-date. + +This decision ensures that the MiniMock framework supports a wide range of property types, providing flexibility and comprehensive mocking capabilities. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/feature/SupportedMemberAccessLevel.md b/docs/ADR/feature/SupportedMemberAccessLevel.md new file mode 100644 index 0000000..61d9d5b --- /dev/null +++ b/docs/ADR/feature/SupportedMemberAccessLevel.md @@ -0,0 +1,31 @@ +# Decision on Accessibility + +## Context + +The accessibility rules that applies to a manual created mock should also apply. No magic reflection logic will be done to access otherwise inaccessible members. + +## Decision + +As such the following will be accessible. + +- **Virtual** and **Abstract** members must be supported. +- **Protected** and **Public** must be supported. + +And the following will not. + +- **Partial**, **Sealed** and None overridable members will not be supported. +- **Internal** and **Private** members will not be exposed. + +## Consequences + +### Positive: + +- **Predictable**: Only exposes the parts of a mock that would normally be exposed. + +### Negative: + +- **Limitation**: Will not support advanced use cases. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/feature/TLDR.md b/docs/ADR/feature/TLDR.md new file mode 100644 index 0000000..875164c --- /dev/null +++ b/docs/ADR/feature/TLDR.md @@ -0,0 +1,43 @@ +# TL;DR + +## Summary of Decisions + +### Decision on Supporting Classes and Interfaces +- **Context**: Determine the scope of support for mocking different types of members. +- **Decision**: Primarily support interfaces but also include support for classes. +- **Consequences**: Flexible and comprehensive but adds complexity and requires ongoing maintenance. + +### Decision on Accessibility +- **Context**: Determine the accessibility rules for mocking. +- **Decision**: Support virtual, abstract, protected, and public members. Do not support partial, sealed, non-overridable, internal, and private members. +- **Consequences**: Predictable behaviour but limited to standard use cases. + +### How to Create Mocks +- **Context**: Establish a standardized approach for creating mocks. +- **Decision**: Use a mock factory for centralized and consistent mock creation but keep constructors accessible. +- **Consequences**: Ensures consistency and ease of use but adds complexity and maintenance requirements. + +### Decision on Supporting Constructors +- **Context**: Determine the scope of support for mocking constructors. +- **Decision**: Support all constructors with the supported access level. If no constructor exists, create a parameterless constructor. Do not generate classes with only internal or private constructors. +- **Consequences**: Simplifies initial implementation but limits advanced usage scenarios. + +### Decision on Supporting Methods +- **Context**: Determine the scope of support for mocking methods. +- **Decision**: Support all standard ways of creating methods, including instance, static, virtual, and abstract methods. Address support for methods returning `ref` values in future updates. +- **Consequences**: Comprehensive and flexible but adds complexity and requires future work for `ref` values. + +### Decision on Supporting Properties +- **Context**: Determine the scope of support for mocking properties. +- **Decision**: Support mocking both read-only and read-write properties in classes and interfaces. +- **Consequences**: Comprehensive and flexible but adds complexity and requires ongoing maintenance. + +### Decision on Supporting Indexers +- **Context**: Determine the scope of support for mocking indexers. +- **Decision**: Support mocking both read-only and read-write indexers. +- **Consequences**: Comprehensive and flexible but adds complexity and requires ongoing maintenance. + +### Decision on Supporting Events +- **Context**: Determine the scope of support for mocking events. +- **Decision**: Support mocking all types of events, including standard, custom, and events with different delegate types. +- **Consequences**: Comprehensive and flexible but adds complexity and requires ongoing maintenance. diff --git a/docs/ADR/general/DoWeNeedANewMockingFramework.md b/docs/ADR/general/DoWeNeedANewMockingFramework.md new file mode 100644 index 0000000..bfbb059 --- /dev/null +++ b/docs/ADR/general/DoWeNeedANewMockingFramework.md @@ -0,0 +1,28 @@ +# Need for a New Mocking Framework + +## Context + +Existing mocking frameworks in the .NET ecosystem often come with clunky APIs and external dependencies that can complicate the development process. These frameworks may offer extensive features, but they can also introduce unnecessary complexity and bloat, making them less suitable for projects that prioritize simplicity and minimalism. + +## Decision + +We will develop a new mocking framework, MiniMock, that focuses on providing a minimalistic and straightforward API. This framework will avoid external dependencies to ensure ease of use and integration. + +## Consequences + +### Positive: + +- **Simplicity**: A minimalistic API will make the framework easier to learn and use, reducing the learning curve for new developers. +- **No External Dependencies**: By avoiding external dependencies, the framework will be easier to integrate and maintain, reducing potential conflicts and bloat. +- **Performance**: A lightweight framework can offer better performance due to reduced overhead. +- **Control**: Greater control over the framework's features and behavior, ensuring it meets the specific needs of the project. + +### Negative: + +- **Feature Limitations**: The framework may lack some advanced features found in more comprehensive mocking frameworks. +- **Development Effort**: Additional effort will be required to develop and maintain the new framework. +- **Adoption**: Convincing developers to switch to a new framework may be challenging, especially if they are accustomed to existing solutions. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/general/DocumentationAndExamples.md b/docs/ADR/general/DocumentationAndExamples.md new file mode 100644 index 0000000..6eb4b50 --- /dev/null +++ b/docs/ADR/general/DocumentationAndExamples.md @@ -0,0 +1,41 @@ +# Approach to Documentation and Examples + +## Context + +Effective documentation and examples are crucial for the adoption and proper use of any framework. Clear and concise documentation helps developers understand the framework's features and usage. Providing relevant examples can further illustrate how to implement and utilize the framework in real-world scenarios. + +## Decision + +We will create concise documentation and examples for the MiniMock framework. The documentation will cover essential aspects of the framework, including installation, configuration, and usage. Examples will be provided to demonstrate common use cases. + +## Consequences + +### Positive: + +- **Clarity**: Clear and concise documentation will help developers understand how to use the framework effectively. +- **Adoption**: Good documentation and examples can increase the adoption rate of the framework. +- **Support**: Reduces the need for support by providing answers to common questions and issues. +- **Consistency**: Ensures consistent usage of the framework across different projects. + +### Negative: + +- **Effort**: Requires effort to create and maintain concise documentation and examples. +- **Maintenance**: Documentation must be kept up-to-date with any changes or updates to the framework. + +## Documentation Structure + +1. **Introduction**: Overview of the framework, its purpose, and key features. +2. **Getting Started**: Instructions on how to install and configure the framework. +3. **Usage Guide**: Detailed guide on how to use the framework, including API references. +4. **Examples**: Practical examples demonstrating common use cases. +5. **FAQ**: Frequently asked questions and troubleshooting tips. + +## Examples + +- **Basic Mocking**: Simple example showing how to create and use a mock. +- **Advanced Mocking**: Examples of more complex scenarios, such as mocking protected methods and handling asynchronous methods. +- **Integration**: Examples showing how to integrate the framework with other tools and libraries. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/general/HowStrictShouldMiniMockBe.md b/docs/ADR/general/HowStrictShouldMiniMockBe.md new file mode 100644 index 0000000..7b1bd64 --- /dev/null +++ b/docs/ADR/general/HowStrictShouldMiniMockBe.md @@ -0,0 +1,26 @@ +# Decision on How Strict the Framework Should Be + +## Context + +In the MiniMock framework, there is a need to determine the level of strictness when handling calls to members that are not explicitly mocked. A strict framework can help catch unintended calls and ensure that tests are precise and reliable. However, events are a special case and should not require listeners when they are called. + +## Decision + +The framework will be strict, throwing exceptions when a member that is not mocked is called. This approach ensures that all interactions are explicitly defined and helps catch unintended calls. However, events will be treated as a special case and will not require listeners when they are called. + +## Consequences + +### Positive: + +- **Precision**: Ensures that all interactions with mocks are explicitly defined, leading to more precise and reliable tests. +- **Error Detection**: Helps catch unintended calls to members that are not mocked, reducing the risk of false positives in tests. +- **Consistency**: Provides a consistent approach to handling calls to non-mocked members. + +### Negative: + +- **Strictness**: The strict approach may require more effort to set up mocks, as all interactions must be explicitly defined. +- **Event Handling**: Special handling for events may introduce some complexity in the framework. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/general/LoggingAndDebugging.md b/docs/ADR/general/LoggingAndDebugging.md new file mode 100644 index 0000000..74f2047 --- /dev/null +++ b/docs/ADR/general/LoggingAndDebugging.md @@ -0,0 +1,35 @@ +# Approach to Logging and Debugging + +## Context + +Effective logging and debugging are essential for the development and maintenance of the MiniMock framework. Logging helps track the setup and usage of mocks, while debugging aids in identifying and resolving issues. To make debugging tests smoother, the `DebuggerStepThrough` attribute will be used to skip over the internal framework code during debugging sessions. + +## Decision + +We will implement logging to capture events related to the setup of mocks and calls to the mocks. Additionally, the `DebuggerStepThrough` attribute will be applied to relevant parts of the framework to streamline the debugging process. Logging functionality will be planned but not yet implemented. + +## Status + +Accepted + +## Consequences + +### Positive: + +- **Traceability**: Logging provides a trace of mock setup and usage, aiding in troubleshooting and analysis. +- **Smooth Debugging**: The `DebuggerStepThrough` attribute helps developers focus on their test code rather than the internal workings of the framework. +- **Insight**: Logs offer insights into the behavior and interactions within the framework. + +### Negative: + +- **Implementation Effort**: Requires effort to implement and maintain logging functionality. +- **Performance Overhead**: Logging may introduce a slight performance overhead. + +## Implementation Plan + +1. **Logging**: Plan and design the logging mechanism to capture mock setup events and calls to mocks. +2. **DebuggerStepThrough**: Apply the `DebuggerStepThrough` attribute to relevant methods and classes to improve the debugging experience. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/general/MatchingTargetApi.md b/docs/ADR/general/MatchingTargetApi.md new file mode 100644 index 0000000..13a8d13 --- /dev/null +++ b/docs/ADR/general/MatchingTargetApi.md @@ -0,0 +1,28 @@ +# Matching Target API in Mock API + +When creating a mocking class, the mock API must closely mirror the API of the target being mocked. +This ensures that the mock can be used as a drop-in replacement for the target, facilitating seamless testing and reducing the learning curve for developers. + +## Decision + +The mock API should reflect the target API with minimal additional methods. +The mock should only include methods that already exist in the target API, ensuring consistency and ease of use. + +## Status + +Accepted + +## Consequences + +### Positive: +- Developers can use the mock without learning a new API. +- Tests are more readable and maintainable as they closely resemble the actual code. +- Reduces the risk of errors due to API mismatches. + +### Negative: +- Limited flexibility in extending the mock API for advanced testing scenarios. +- May require more effort to implement certain mocking features without additional methods. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/general/NoBuiltInAssertionFeature.md b/docs/ADR/general/NoBuiltInAssertionFeature.md new file mode 100644 index 0000000..4655e64 --- /dev/null +++ b/docs/ADR/general/NoBuiltInAssertionFeature.md @@ -0,0 +1,27 @@ +# No Built-in Assertion Feature + +## Context + +In our mocking framework, there is a consideration to include a built-in assertion feature. However, there are numerous assertion frameworks available, each with its own strengths and user base. +Including a built-in assertion feature may lead to redundancy and limit the flexibility for users to choose their preferred assertion framework. + +## Decision + +We will not include a built-in assertion feature in our mocking framework. Instead, we will rely on users to choose and use their preferred assertion framework. + +## Consequences + +### Positive: + +- **Flexibility**: Users can choose the assertion framework that best fits their needs and preferences. +- **Simplicity**: Reduces the complexity of the mocking framework by not including redundant features. +- **Interoperability**: Ensures compatibility with a wide range of existing assertion frameworks. + +### Negative: + +- **Learning Curve**: Users may need to learn and integrate a separate assertion framework if they are not already familiar with one. +- **Dependency Management**: Users will need to manage additional dependencies for their chosen assertion framework. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/general/NoDependencies.md b/docs/ADR/general/NoDependencies.md new file mode 100644 index 0000000..24db71e --- /dev/null +++ b/docs/ADR/general/NoDependencies.md @@ -0,0 +1,28 @@ +# No Dependencies to Shared Libraries + +## Context + +In our project, there is a consideration to include dependencies on shared libraries. However, relying on shared libraries can introduce several challenges, including version conflicts, increased complexity, and reduced control over the project's dependencies. + +## Decision + +We will not include any dependencies on shared libraries in our project. Instead, we will aim to implement necessary functionality within the project itself using source generation. + +## Consequences + +### Positive: + +- **Control**: Greater control over the project's dependencies and versions. +- **Simplicity**: Reduces the complexity of managing external dependencies. +- **Stability**: Minimizes the risk of version conflicts and compatibility issues. +- **Security**: Reduces the attack surface by limiting external dependencies. + +### Negative: + +- **Development Effort**: May require additional effort to implement functionality that would otherwise be provided by shared libraries. +- **Code Duplication**: Potential for code duplication if similar functionality is needed across multiple projects. +- **Maintenance**: Increased maintenance burden as all functionality must be maintained within the project. + +--- + +More ADRs can be found in the [docs/ADR](../README.md) directory. diff --git a/docs/ADR/general/TLDR.md b/docs/ADR/general/TLDR.md new file mode 100644 index 0000000..de9ee5b --- /dev/null +++ b/docs/ADR/general/TLDR.md @@ -0,0 +1,46 @@ +# TL;DR + +## General concerns + +### Need for a New Mocking Framework +- **Context**: Existing frameworks are complex and have external dependencies. +- **Decision**: Develop MiniMock with a minimalistic API and no external dependencies. +- **Consequences**: Simplifies usage and improves performance but may lack advanced features and require development effort. + +### Supported Features +- **Context**: Which features should be supported, which should not and where do we set the bar. +- **Decision**: Common features that can be used in interfaces and classes should be supported but no 'magic' must be used. +- **Consequences**: Ensures predictability and consistency but limits the usage to what standard scenarios. + +### How Strict the Framework Should Be +- **Context**: Determine strictness for handling calls to non-mocked members. +- **Decision**: Be strict, throwing exceptions for non-mocked members, with special handling for events. +- **Consequences**: Ensures precision and error detection but requires more setup effort and special event handling. + +### Matching Target API in Mock API +- **Context**: Mock API should closely mirror the target API. +- **Decision**: Reflect the target API with minimal additional methods. +- **Consequences**: Ensures consistency and ease of use but limits flexibility and may require more effort for certain features. + +### No Dependencies on Shared Libraries +- **Context**: Consideration of including dependencies on shared libraries. +- **Decision**: Do not include dependencies on shared libraries; implement functionality within the project or use static linking. +- **Consequences**: Provides control and stability but requires additional development effort and maintenance. + + +### Documentation and Examples +- **Context**: Effective documentation and examples are crucial for adoption and proper use. +- **Decision**: Create concise documentation and examples covering installation, configuration, usage, and common use cases. +- **Consequences**: Enhances clarity and adoption but requires effort and maintenance. + + +### Logging and Debugging +- **Context**: Effective logging and debugging are essential. +- **Decision**: Implement logging and use `DebuggerStepThrough` for smoother debugging. +- **Consequences**: Improves traceability and debugging but requires implementation effort and may introduce performance overhead. + +### No Built-in Assertion Feature +- **Context**: Consideration of including a built-in assertion feature. +- **Decision**: Do not include a built-in assertion feature; rely on external assertion frameworks. +- **Consequences**: Provides flexibility and simplicity but requires users to manage additional dependencies. + diff --git a/Documentation/AnalyzerRules/MM0001.md b/docs/AnalyzerRules/MM0001.md similarity index 100% rename from Documentation/AnalyzerRules/MM0001.md rename to docs/AnalyzerRules/MM0001.md diff --git a/Documentation/AnalyzerRules/MM0002.md b/docs/AnalyzerRules/MM0002.md similarity index 100% rename from Documentation/AnalyzerRules/MM0002.md rename to docs/AnalyzerRules/MM0002.md diff --git a/Documentation/AnalyzerRules/MM0003.md b/docs/AnalyzerRules/MM0003.md similarity index 100% rename from Documentation/AnalyzerRules/MM0003.md rename to docs/AnalyzerRules/MM0003.md diff --git a/Documentation/AnalyzerRules/MM0004.md b/docs/AnalyzerRules/MM0004.md similarity index 100% rename from Documentation/AnalyzerRules/MM0004.md rename to docs/AnalyzerRules/MM0004.md diff --git a/Documentation/AnalyzerRules/MM0005.md b/docs/AnalyzerRules/MM0005.md similarity index 100% rename from Documentation/AnalyzerRules/MM0005.md rename to docs/AnalyzerRules/MM0005.md diff --git a/Documentation/AnalyzerRules/MM0006.md b/docs/AnalyzerRules/MM0006.md similarity index 100% rename from Documentation/AnalyzerRules/MM0006.md rename to docs/AnalyzerRules/MM0006.md diff --git a/Documentation/OtherLanguageWorkaround.md b/docs/OtherLanguageWorkaround.md similarity index 100% rename from Documentation/OtherLanguageWorkaround.md rename to docs/OtherLanguageWorkaround.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..5c79c22 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,38 @@ +# MiniMock + +MiniMock offers a **minimalistic** approach to mocking in .NET with a focus on simplicity and ease of use. + +```csharp + public interface IBookRepository + { + Task AddBook(Book book, CancellationToken token); + int BookCount { get; set; } + Book this[Guid index] { get; set; } + event EventHandler NewBookAdded; + } + + [Fact] + [Mock] + public async Task BookCanBeCreated() + { + Action trigger = _ => { }; + + var mockRepo = Mock.IBookRepository(config => config + .AddBook(returns: Guid.NewGuid()) + .BookCount(value: 10) + .Indexer(values: new Dictionary()) + .NewBookAdded(trigger: out trigger)); + + var sut = new BookModel(mockRepo); + var actual = await sut.AddBook(new Book()); + + Assert.Equal("We now have 10 books", actual); + } +``` + +Try it out or continue with [Getting started](guide/getting-started.md) to learn more or read the [Mocking guidelines](guide/mocking-guidelines.md) to get a better understanding of when, why and how to mock and when not to. + +For more details on specific aspects you can read about [Construction](guide/construction.md), [Methods](guide/methods.md), [Properties](guide/properties.md), [Events](guide/events.md) or +[Indexers](guide/indexers.md). + +If you are more into the ins and outs of MiniMock you can read the [ADRs](ADR/README.md). diff --git a/docs/Troll.jpg b/docs/Troll.jpg new file mode 100644 index 0000000..6d29ce1 Binary files /dev/null and b/docs/Troll.jpg differ diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..363f7fd --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,6 @@ +# _config.yml +title: MiniMock +description: Minimalistic approach to mocking in .NET +remote_theme: pages-themes/hacker@v0.2.0 +plugins: + - jekyll-remote-theme # add this line to the plugins list if you already have one diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html new file mode 100644 index 0000000..b675933 --- /dev/null +++ b/docs/_layouts/default.html @@ -0,0 +1,42 @@ + + + + + + + + + {% include head-custom.html %} + + {% seo %} + + + + +
+
+ +

{{ site.title | default: site.github.repository_name }}

+
+

{{ site.description | default: site.github.project_tagline }}

+ +
+ {% if site.show_downloads %} + Download as .zip + Download as .tar.gz + {% endif %} + View on GitHub +
+ +
+
+
+
+
+ {{ content }} +
+
+ + diff --git a/docs/assets/css/custom.css b/docs/assets/css/custom.css new file mode 100644 index 0000000..3354c4b --- /dev/null +++ b/docs/assets/css/custom.css @@ -0,0 +1,14 @@ +strong { + color:#b5e853 +} + +header #links { + position: relative; + height: 0px; +} + +header { + border-bottom: 0px dashed #b5e853; + padding: 10px 0; + margin: 0 0 10px 0 +} diff --git a/docs/guide/Documentation.png b/docs/guide/Documentation.png new file mode 100644 index 0000000..ae0339e Binary files /dev/null and b/docs/guide/Documentation.png differ diff --git a/docs/guide/Exception.png b/docs/guide/Exception.png new file mode 100644 index 0000000..9d014b6 Binary files /dev/null and b/docs/guide/Exception.png differ diff --git a/docs/guide/construction.md b/docs/guide/construction.md new file mode 100644 index 0000000..e63f182 --- /dev/null +++ b/docs/guide/construction.md @@ -0,0 +1,27 @@ +# Construction + +__TL;DR__ + +```csharp +[Fact] +[Mock] // Signals minimock to build a mock object for IVersionLibrary. +public void MockInitialization() +{ + // Mock without anything mocked used to satisfy dependencies not used in the tests execution path + var emptyMock = Mock.IVersionLibrary(); + + // Mock with inline configuration useful for most setup scenarios + var inlineMock = Mock.IVersionLibrary(config => config + .DownloadExists(true) + ); + + // Mock with external configuration useful for more complex scenarios like testing events and modifying mock behaviour. + var externalMock = Mock.IVersionLibrary(out var config); + config.DownloadExists(true); + + // Direct access to the mock implementation. + var implementationMock = new MockOf_IVersionLibrary(config => config + .DownloadExists(true) + ); +} +``` diff --git a/docs/guide/events.md b/docs/guide/events.md new file mode 100644 index 0000000..5430fc6 --- /dev/null +++ b/docs/guide/events.md @@ -0,0 +1,35 @@ +# Events + +__TL;DR__ + +```csharp +public interface IVersionLibrary +{ + event EventHandler NewVersionAdded; +} + +Action triggerNewVersionAdded = _ => { }; + +var versionLibrary = Mock.IVersionLibrary(config => config + .NewVersionAdded(raise: new Version(2, 0, 0, 0)) // Raises the event right away + .NewVersionAdded(trigger: out triggerNewVersionAdded) // Provides a trigger for when a new version is added +); + +// Inject into system under test + +triggerNewVersionAdded(new Version(2, 0, 0, 0)); +``` + +Alternative to creating a action for triggering the is to use the out parameter for configuration instead. +```csharp +var versionLibrary = Mock.IVersionLibrary(out var config); + +// Inject into system under test + +config.NewVersionAdded(raise: new Version(2, 0, 0, 0)); +``` + +__Please note__ + +- Parameter-names can be omitted but makes the code more readable. +- Unlike other members, events does not need to be specified in order to be subscribed to by the system under test. diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md new file mode 100644 index 0000000..266db97 --- /dev/null +++ b/docs/guide/getting-started.md @@ -0,0 +1,46 @@ +# Getting started + +## Installation and First Use + +Reference the NuGet package in your test project: + +```sh +dotnet add package MiniMock +``` + +- Specify which interface to mock by using the `[Mock]` attribute before your test or test class: +- Create a new instance of the mock using the `Mock.IMyRepository()` factory method. +- Configure the mock using the `config` parameter of the factory method. +- Specify how the relevant members should behave using the members name and specify the behavior using the parameters. +- Use the mock in your test as you see fit. + +As code: + +```csharp +[Fact] +[Mock] // Specify which interface to mock +public void MyTest() { + var mockRepo = Mock.IMyRepository(// Create a new instance of the mock using the mock factory + config => config // Configure the mock using the config parameter + .CreateCustomerAsync(return: Guid.NewGuid()) // Specify how the relevant members should behave + ); + var sut = new CustomerMaintenance(mockRepo); // Use the mock in your test as you see fit + + sut.Create(customerDTO, cancellationToken); +} +``` + +## Quality of Life + +MiniMock is **extremely strict** but **fair**, requiring you to specify all features you want to mock but giving you fair warnings if you don't. +This is by design to make sure you are aware of what you are mocking and not introduce unexpected behaviour. + +![exception](Exception.png) + +All mockable members are available through a **fluent interface** with **IntelliSense**, **type safety**, and **documentation**. + +![documentation](Documentation.png) + +All code required to run MiniMock is **source generated** within your test project and has **no runtime dependencies**. You can **inspect**, **step into**, and **debug** the generated code which also allows for **security** and **vulnerability +scanning** of the generated code. + diff --git a/docs/guide/indexers.md b/docs/guide/indexers.md new file mode 100644 index 0000000..0e24293 --- /dev/null +++ b/docs/guide/indexers.md @@ -0,0 +1,25 @@ +# Indexers + +__TL;DR__ + +```csharp +public interface IVersionLibrary +{ + Version this[string key] { get; set; } +} + +var versions = new Dictionary { { "current", new Version(2, 0, 0, 0) } }; + +var versionLibrary = Mock.IVersionLibrary(config => config + .Indexer(get: key => new Version(2, 0, 0, 0), set: (key, value) => { }) // Overwrites the indexer getter and setter + .Indexer(values: versions) // Provides a dictionary to retrieve and store versions +); + +// Inject into system under test +``` + +__Please note__ + +- Multiple specifications for an indexer will overwrite each other with the last one taking precedence. +- Parameter-names can be omitted but make the code more readable. +- Any indexer that is not explicitly specified will throw an `InvalidOperationException` when called. diff --git a/docs/guide/methods.md b/docs/guide/methods.md new file mode 100644 index 0000000..0b7af0b --- /dev/null +++ b/docs/guide/methods.md @@ -0,0 +1,105 @@ +# Methods + +__TL;DR__ + +```csharp +public interface IVersionLibrary +{ + bool DownloadExists(string version); + bool DownloadExists(Version version); + Task DownloadLinkAsync(string version); + } + +var versionLibrary = Mock.IVersionLibrary(config => config + .DownloadExists(returns: true) // Returns true for all overloads + .DownloadExists(throws: new IndexOutOfRangeException()) // Throws IndexOutOfRangeException for all overloads + .DownloadExists(call: s => s.StartsWith(value: "2.0.0")) // Returns true for version 2.0.0.x base on a string parameter + .DownloadExists(call: v => v is { Major: 2, Minor: 0, Revision: 0 }) // Returns true for version 2.0.0.x based on a version parameter + .DownloadExists(returnValues: [true, true, false]) // Returns true two times, then false + + .DownloadLinkAsync(returns: Task.FromResult(result: new Uri(uriString: "http://downloads/2.0.0"))) // Returns a task containing a download link for all versions + .DownloadLinkAsync(call: s => Task.FromResult(result: s.StartsWith(value: "2.0.0") ? new Uri(uriString: "http://downloads/2.0.0") : new Uri(uriString: "http://downloads/UnknownVersion"))) // Returns a task containing a download link for version 2.0.0.x otherwise a error link + .DownloadLinkAsync(throws: new TaskCanceledException()) // Throws IndexOutOfRangeException for all parameters + .DownloadLinkAsync(returns: new Uri(uriString: "http://downloads/2.0.0")) // Returns a task containing a download link for all versions + .DownloadLinkAsync(call: s => s.StartsWith(value: "2.0.0") ? new Uri(uriString: "http://downloads/2.0.0") : new Uri(uriString: "http://downloads/UnknownVersion")) // Returns a task containing a download link for version 2.0.0.x otherwise a error link + .DownloadLinkAsync(returnValues: [Task.FromResult(result: new Uri(uriString: "http://downloads/1.0.0")), Task.FromResult(result: new Uri(uriString: "http://downloads/1.1.0")), Task.FromResult(result: new Uri(uriString: "http://downloads/2.0.0"))]) // Returns a task with a download link + .DownloadLinkAsync(returnValues: [new Uri(uriString: "http://downloads/2.0.0"), new Uri(uriString: "http://downloads/2.0.0"), new Uri(uriString: "http://downloads/2.0.0")]) // Returns a task with a download link +); + +// Inject into system under test +``` + +__Please note__ + +- Multiple specifications for a method will overwrite each other with the last one taking precedence. +- Parameter-names can be omitted but makes the code more readable. +- Any method that is not explicitly specified will throw a `InvalidOperationException` when called. + +## Common scenarios + +__Call lambda expression or method__ to specify what should happen based on the input parameter. +This can be done by using a lambda expression or a method and offers flexibility and control over what happens when the method is called, but also requires more code. + +```csharp +.DownloadExists(call: s => s.StartsWith("2.0.0")) // Returns true for version 2.0.0.x based on a string parameter +``` + +```csharp +.DownloadExists(call: MockDownloadExists); + +private bool MockDownloadExists(Version version) +{ + return version is { Major: 2, Minor: 0, Revision: 0 }; +} +``` + +__Return a fixed value__ for any call to the method gives a quick and easy way to specify the return value when you don't care about the input parameters. + +```csharp +.DownloadExists(returns: true) // Returns true for all parameters +``` + +__Return multiple values__ for a method when you need to wary the result for each call. The first value is returned for the first call, the second for the second call, and so on. +When the last value is reached an exception is thrown. + + ```csharp + .DownloadExists(returnValues: [true, true, false]) // Returns true two times, then false + ``` + +__Methods that return void__ can be mocked by not specifying any parameters. + +```csharp +.LogDownloadRequest() +``` + +__Throwing exceptions__ can be done by specifying the exception to throw for any call to the method. + +```csharp +.DownloadExists(throws: new IndexOutOfRangeException()) // Throws IndexOutOfRangeException for all versions +``` + +## Overloaded methods + +When a method is overloaded, you can specify the return value for one specific overload based on the input parameter. + +```csharp +.DownloadExists(call: v => v is { Major: 2, Minor: 0, Revision: 0 }) // Returns true for version 2.0.0.x based on a version parameter +.DownloadExists(call: s => s.StartsWith("2.0.0")) // Returns true for version 2.0.0.x base on a string parameter +``` + +For overloaded methods returning identical values all overloaded methods return values will be set using the return and return values parameters. + +Specifying the throw parameter will throw the exception for all overloaded methods. + +Returning multiple values for overloaded methods will handle each overload as a separate instance. + +## Async methods + +Methods returning a `Task` or `Task` are supported by the common scenarios but also supports specifying the return value or values without the Task. + +```csharp +.DownloadExistsAsync(call: s => s.StartsWith("2.0.0")) // Returns true for version 2.0.0.x that will be wrapped in a task +.DownloadExistsAsync(returns: true) // Returns true for any parameter that will be wrapped in a task +.DownloadExistsAsync(returnValues: [true, false, true]) // Returns true, false, true for the first, second, and third call that will be wrapped in a task +.LogDownloadRequestAsync() // Returns a completed task for all parameters +``` diff --git a/docs/guide/mocking-guidelines.md b/docs/guide/mocking-guidelines.md new file mode 100644 index 0000000..6a91a48 --- /dev/null +++ b/docs/guide/mocking-guidelines.md @@ -0,0 +1,152 @@ +# Mocking Guidelines + +Mocking is a crucial aspect of unit testing, allowing developers to isolate and test individual components of their applications. +This document provides __opinionated guidelines__ on how to effectively use mocks to mock various targets, ensuring comprehensive and +reliable tests. + +Just as important as knowing what to mock is knowing what not to mock. This document also outlines objects that should not be mocked. + +## Good Candidates for Mocking + +When writing unit tests, it's important to mock certain components to isolate the unit of work being tested. Here are some good candidates for mocking: + +### External Dependencies + +External dependencies include databases, file systems, web services, and any other external systems that your code interacts with. +Mocking these dependencies allows you to test your code without relying on the actual external systems. +This ensures that your tests are not affected by the availability or state of these external systems, leading to more reliable and faster tests. + +Remember to test the actual interactions with external dependencies in integration or end-to-end tests to ensure that your code works correctly with the real systems. + +### Planned Components + +Components that are planned but not yet implemented, such as interfaces or classes that are part of the design but not yet developed, can be mocked to test how your code interacts with them. +By mocking these components, you can simulate their behavior and test how your code integrates with them before they are fully implemented. +This helps in identifying potential issues early in the development process. + +Remember to update the mocks when the actual components are implemented to ensure that the tests remain relevant and accurate. + +### Time-consuming Operations + +Operations that are time-consuming, such as network requests, complex calculations or database queries, can be mocked to speed up test execution. +By mocking these operations, you can simulate their behavior without actually performing the time-consuming tasks, which helps in running tests quickly and efficiently. + +Remember to test the actual time-consuming operations in integration or end-to-end tests to ensure that they work correctly. +You can use the mock to test timeout scenarios or error handling without relying on the actual time-consuming operations. + +### Volatile Components + +Components that return volatile or unpredictable results, such as random number generators and guid generators should be mocked to ensure that your tests are deterministic and reproducible. +By mocking these components, you can control the output and simulate different scenarios to test how your code behaves under various conditions. + +Remember to test the actual volatile components in integration or end-to-end tests to ensure that your code works correctly with the real components. +If the components are volatile in production scenarios, remember to test the edge cases in your code. + +### Error-prone Components + +Components that are error-prone, such as third-party libraries, legacy code, or complex algorithms, should be mocked to test how your code behaves under different error conditions. +By mocking these components, you can simulate error scenarios and ensure that your code handles errors correctly without relying on the actual error-prone components. + +Remember to test the actual error-prone components in integration or end-to-end tests to ensure that your code works correctly with the real components. +If the components are error-prone in production scenarios remember to test the error handling logic in your code. + +## Specific Examples + +### Services + +Any service classes that your code depends on, such as authentication services, payment gateways, or email services, should be mocked to isolate the unit of work. +This allows you to test the logic of your code without relying on the actual implementation of these services, ensuring that your tests are focused and reliable. + +Consider isolating the service dependencies behind a facade or adapter to isolate our code from the actual service implementation. + +### Repositories + +Data access layers or repositories that interact with the database should be mocked to test the business logic without hitting the database. +This helps in isolating the business logic from the data access logic, making your tests more focused and faster. + +Using an in-memory database can be an alternative to mocking repositories, but makes is harder to run tests in parallel and can be slower than using mocks. + +### Caching Layers + +Mocking caching mechanisms like `IMemoryCache` or `IDistributedCache` allows you to test how your application behaves with different cache states. +This helps in ensuring that your application handles caching correctly without relying on the actual cache implementation. + +For scenarios where the cache is not the focus of the test cases, consider using a simple in-memory cache instead of mocking the cache. + +### Message Queues + +Mocking message queue clients like `IQueueClient` or `IMessagePublisher` allows you to simulate message sending and receiving without relying on the actual message broker. +This helps in testing the messaging logic of your application in isolation. + +### User Context + +Mocking user context providers like `IHttpContextAccessor` allows you to simulate different user scenarios and authentication states. +This helps in testing how your application behaves under different user contexts without relying on the actual user context implementation. + + +By mocking these targets, you can create isolated and reliable unit tests that focus on the specific behavior of the code under test. + +## Do Not Mock + +When writing unit tests, certain components should generally not be mocked: + +### Objects with Existing Test Classes + +Objects like `ILogger`, `TimeProvider`, and `HttpClient` already have well-defined test classes provided by the framework or libraries. +These test classes are designed to facilitate testing without the need for mocking. +Using these existing test classes ensures that your tests are more reliable and maintainable. + +### Mapper Classes + +Mapper classes are responsible for converting data between different layers of your application. +Instead of mocking these classes, use the real mapper classes and test the mapping logic. +This ensures that the mappings are correct and that any changes to the mapping logic are properly tested. + +### Validation Logic + +Validation logic, such as Fluent Validation, data annotations or custom validation attributes is part of the behavior of your application and should not be mocked. +Instead of mocking the validation logic, test the validation rules directly by passing valid and invalid data to the validation logic and asserting the results. + +### Value Objects + +Value objects are simple data structures like `DateTime`, `TimeSpan`, or custom value objects. +These objects should not be mocked because they are simple and have no behavior that needs to be isolated. +Use real instances of these objects in your tests to ensure that they are used correctly. + +### Third-Party Libraries + +Avoid mocking third-party libraries directly. Instead, create an abstraction layer around the third-party library and mock that layer. +This approach ensures that your tests are not tightly coupled to the third-party library and that you can easily replace the library if needed. + +### Configuration Settings + +Use real configuration settings in your tests to ensure that they are correctly applied. +Mocking configuration settings can lead to tests that do not accurately reflect the real behavior of your application. +By using real configuration settings, you can ensure that your tests are more reliable and maintainable. + +Mocking these objects can lead to brittle tests that are tightly coupled to the implementation details of the code under test. + +## Unsupported Mocking Scenarios + +The following scenarios are generally not recommended for mocking and not supported by the MiniMock framework: + +### Static Methods and classes + +MiniMock uses inheritance-based mocking, which does not support mocking static methods or classes. +Mocking static methods can lead to brittle tests and should be avoided. +Static methods are tightly coupled to their class and cannot be easily replaced or overridden. +Instead, refactor the code to use dependency injection, allowing you to inject dependencies that can be mocked. + +### Private Methods + +Private methods should be tested indirectly through the public methods that call them. +Testing private methods directly can lead to tests that are tightly coupled to the implementation details, making them brittle and harder to maintain. + +### Extension Methods + +Extension methods should be tested through the classes they extend rather than mocked directly. This ensures that the extension methods are tested in the context of their usage. + +### Sealed Classes + +MiniMock uses inheritance-based mocking, which does not support mocking sealed classes. +Consider using interfaces or abstractions to enable mocking. This allows you to create mock implementations for testing purposes. diff --git a/docs/guide/properties.md b/docs/guide/properties.md new file mode 100644 index 0000000..928b50f --- /dev/null +++ b/docs/guide/properties.md @@ -0,0 +1,25 @@ +# Properties + +__TL;DR__ + +```csharp +public interface IVersionLibrary +{ + Version CurrentVersion { get; set; } +} + +var versionLibrary = Mock.IVersionLibrary(config => config + .CurrentVersion(get: () => new Version(major: 2, minor: 0, build: 0, revision: 0), set: version => throw new IndexOutOfRangeException()) // Overwrites the property getter and setter + .CurrentVersion(value: new Version(major: 2, minor: 0, build: 0, revision: 0)) // Sets the initial version to 2.0.0.0 +); + +// Inject into system under test + +``` + +__Please note__ + +- Multiple specifications for a property will overwrite each other with the last one taking precedence. +- Parameter-names can be omitted but make the code more readable. +- Any property that is not explicitly specified will throw an `InvalidOperationException` when called. +- If the mocked interface or class only exposes get or set only the exposes parameter will be shown. diff --git a/tests/MiniMock.Tests/Demo.cs b/tests/MiniMock.Tests/Demo.cs index ce4a19f..300555a 100644 --- a/tests/MiniMock.Tests/Demo.cs +++ b/tests/MiniMock.Tests/Demo.cs @@ -70,6 +70,28 @@ public async Task TestingAllTheOptions() triggerNewVersionAdded(new Version(2, 0, 0, 0)); } + [Fact] + [Mock] + public void MockInitialization() + { + // Mock without anything mocked used to satisfy dependencies not used in the tests execution path + var emptyMock = Mock.IVersionLibrary(); + + // Mock with inline configuration useful for most setup scenarios + var inlineMock = Mock.IVersionLibrary(config => config + .DownloadExists(true) + ); + + // Mock with external configuration useful for more complex scenarios like testing events and modifying mock behaviour. + var externalMock = Mock.IVersionLibrary(out var config); + config.DownloadExists(true); + + // Direct access to the mock implementation. + var implementationMock = new MockOf_IVersionLibrary(config => config + .DownloadExists(true) + ); + } + [Fact] [Mock] public void SimpleReturnValue()