|
1 | 1 | # MiniMock |
2 | 2 |
|
3 | | -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. |
4 | | - |
5 | | -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. |
6 | | - |
7 | | -## Table of Contents |
8 | | -- [Simple Example](#simple-example) |
9 | | -- [Key Feature Summary](#key-feature-summary) |
10 | | -- [Limitations](#limitations) |
11 | | -- [Installation & Initialization](#installation--initialization) |
12 | | -- [Quality of Life Features](#quality-of-life-features) |
13 | | - - [Fluent Interface](#fluent-interface) |
14 | | - - [Simple Return Values](#simple-return-values) |
15 | | - - [Multiple Return Values](#multiple-return-values) |
16 | | - - [Intercept Method Calls](#intercept-method-calls) |
17 | | - - [Async Methods](#async-methods) |
18 | | - - [Strict Mocking](#strict-mocking) |
19 | | - - [Adding Indexers](#adding-indexers) |
20 | | - - [Raising Events](#raising-events) |
21 | | - - [Argument Matching](#argument-matching) |
22 | | - |
23 | | -## Simple Example |
24 | | - |
25 | | -```csharp |
26 | | -public interface IVersionLibrary |
27 | | -{ |
28 | | - bool DownloadExists(string version); |
29 | | -} |
30 | | - |
31 | | -[Fact] |
32 | | -[Mock<IVersionLibrary>] |
33 | | -public void SimpleExample() |
34 | | -{ |
35 | | - var library = Mock.IVersionLibrary(config => |
36 | | - config.DownloadExists(returns: true)); |
37 | | - |
38 | | - var actual = library.DownloadExists("2.0.0.0"); |
39 | | - |
40 | | - Assert.True(actual); |
41 | | -} |
42 | | -``` |
43 | | - |
44 | | -## Key Feature Summary |
45 | | - |
46 | | -- Minimalistic API with fluent method chaining, documentation, and full IntelliSense |
47 | | -- Mocking of interfaces, abstract classes, and virtual methods (with limitations) |
48 | | -- Mocking of methods, properties, indexers, and events |
49 | | -- Simple factory methods to initialize mocks |
50 | | -- Mocking of async methods, overloads, and generic methods |
51 | | -- Ref and out parameters in methods supported |
52 | | -- Generic interfaces supported |
53 | | - |
54 | | -## Limitations |
55 | | - |
56 | | -- No validation of calls |
57 | | -- Only supports C# (workarounds exist for VB.NET and F#) |
58 | | -- Ref return values and ref properties are not supported ([issue #5](https://github.com/oswaldsql/MiniMock/issues/5)) |
59 | | -- Partial mocking of classes |
60 | | - - Base classes with constructors with parameters are not currently supported ([Issue #4](https://github.com/oswaldsql/MiniMock/issues/4)) |
61 | | -- No support for static classes or methods |
62 | | - |
63 | | -## Installation & Initialization |
64 | | - |
65 | | -Reference the NuGet package in your test project: |
66 | | - |
67 | | -```sh |
68 | | -dotnet add package MiniMock |
69 | | -``` |
70 | | - |
71 | | -Specify which interface to mock by using the `[Mock<T>]` attribute before your test or test class: |
72 | | - |
73 | | -```csharp |
74 | | -[Fact] |
75 | | -[Mock<IMyRepository>] // Specify which interface to mock |
76 | | -public void MyTest() { |
77 | | - var mockRepo = Mock.IMyRepository(config => config // Create a mock using the mock factory |
78 | | - .CreateCustomerAsync(return: Guid.NewGuid()) // Configure your mock to your needs |
79 | | - ); |
80 | | - var sut = new CustomerMaintenance(mockRepo); // Inject the mock into your system under test |
81 | | - |
82 | | - sut.Create(customerDTO, cancellationToken); |
83 | | -} |
84 | | -``` |
85 | | - |
86 | | -## Quality of Life Features |
87 | | - |
88 | | -### Fluent Interface |
89 | | - |
90 | | -All mockable members are available through a _fluent interface_ with _IntelliSense_, _type safety_, and _documentation_. |
91 | | - |
92 | | -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. |
93 | | - |
94 | | -All code required to run MiniMock is generated and has _no runtime dependencies_. |
95 | | - |
96 | | -### Simple Return Values |
97 | | - |
98 | | -Simply specify what you expect returned from methods or properties. All parameters are ignored. |
| 3 | +MiniMock offers a _minimalistic_ approach to mocking in .NET with a focus on simplicity and ease of use. |
99 | 4 |
|
100 | 5 | ```csharp |
101 | | -var mockLibrary = Mock.IVersionLibrary(config => config |
102 | | - .DownloadExists(returns: true) // Returns true for any parameter |
103 | | - .DownloadLinkAsync(returns: new Uri("http://downloads/2.0.0")) // Returns a task with a download link |
104 | | - .CurrentVersion(value: new Version(2, 0, 0, 0)) // Sets the initial version to 2.0.0.0 |
105 | | - .Indexer(values: versions) // Provides a dictionary to retrieve and store versions |
106 | | -); |
107 | | -``` |
108 | | - |
109 | | -### Multiple Return Values |
110 | | - |
111 | | -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. |
112 | | - |
113 | | -```csharp |
114 | | -var mockLibrary = Mock.IVersionLibrary(config => config |
115 | | - .DownloadExists(returnValues: true, false, true) // Returns true, false, true for the first, second, and third call |
116 | | - .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 |
117 | | - .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 |
118 | | -); |
119 | | -``` |
120 | | - |
121 | | -### Intercept Method Calls |
122 | | - |
123 | | -```csharp |
124 | | -[Fact] |
125 | | -[Mock<IVersionLibrary>] |
126 | | -public async Task InterceptMethodCalls() |
127 | | -{ |
128 | | - var currentVersionMock = new Version(2, 0, 0); |
129 | | - |
130 | | - var versionLibrary = Mock.IVersionLibrary(config => config |
131 | | - .DownloadExists(call: (string s) => s.StartsWith("2.0.0") ? true : false) // Returns true for version 2.0.0.x based on a string parameter |
132 | | - .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 |
133 | | - // or |
134 | | - .DownloadExists(call: LocalIntercept) // Calls a local function |
135 | | - .DownloadExists(call: version => this.ExternalIntercept(version, true)) // Calls function in class |
136 | | -
|
137 | | - .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 |
138 | | - .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 |
139 | | -
|
140 | | - .CurrentVersion(get: () => currentVersionMock, set: version => currentVersionMock = version) // Overwrites the property getter and setter |
141 | | - .Indexer(get: s => new Version(2, 0, 0, 0), set: (s, version) => {}) // Overwrites the indexer getter and setter |
142 | | - ); |
143 | | - |
144 | | - return; |
145 | | - |
146 | | - bool LocalIntercept(Version version) |
| 6 | + public interface IBookRepository |
147 | 7 | { |
148 | | - return version is { Major: 2, Minor: 0, Revision: 0 }; |
| 8 | + Task<Guid> AddBook(Book book, CancellationToken token); |
| 9 | + int BookCount { get; set; } |
| 10 | + Book this[Guid index] { get; set; } |
| 11 | + event EventHandler<Book> NewBookAdded; |
149 | 12 | } |
150 | | -} |
151 | | - |
152 | | -private bool ExternalIntercept(string version, bool startsWith) => startsWith ? version.StartsWith("2.0.0") : version == "2.0.0"; |
153 | | -``` |
154 | | - |
155 | | -### Async Methods |
156 | | - |
157 | | -Simply return what you expect from async methods either as a `Task` object or a simple value. |
158 | | - |
159 | | -```csharp |
160 | | -var versionLibrary = Mock.IVersionLibrary(config => config |
161 | | - .DownloadLinkAsync(returns: Task.FromResult(new Uri("http://downloads/2.0.0"))) // Returns a task containing a download link for all versions |
162 | | - .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 |
163 | | - // or |
164 | | - .DownloadLinkAsync(returns: new Uri("http://downloads/2.0.0")) // Returns a task containing a download link for all versions |
165 | | - .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 |
166 | | -); |
167 | | -``` |
168 | | - |
169 | | -### Strict Mocking |
170 | | - |
171 | | -Unmocked features will always throw `InvalidOperationException`. |
172 | | - |
173 | | -```csharp |
174 | | -[Fact] |
175 | | -[Mock<IVersionLibrary>] |
176 | | -public void UnmockedFeaturesAlwaysThrowInvalidOperationException() |
177 | | -{ |
178 | | - var versionLibrary = Mock.IVersionLibrary(); |
179 | | - |
180 | | - var propertyException = Assert.Throws<InvalidOperationException>(() => versionLibrary.CurrentVersion); |
181 | | - var methodException = Assert.Throws<InvalidOperationException>(() => versionLibrary.DownloadExists("2.0.0")); |
182 | | - var asyncException = Assert.ThrowsAsync<InvalidOperationException>(() => versionLibrary.DownloadLinkAsync("2.0.0")); |
183 | | - var indexerException = Assert.Throws<InvalidOperationException>(() => versionLibrary["2.0.0"]); |
184 | | -} |
185 | | -``` |
186 | | - |
187 | | -### Adding Indexers |
188 | | - |
189 | | -Mocking indexers is supported either by overloading the get and set methods or by providing a dictionary with expected values. |
190 | 13 |
|
191 | | -```csharp |
192 | | -[Fact] |
193 | | -[Mock<IVersionLibrary>] |
194 | | -public void Indexers() |
195 | | -{ |
196 | | - var versions = new Dictionary<string, Version>() {{"current", new Version(2,0,0,0)}}; |
197 | | - |
198 | | - var versionLibrary = Mock.IVersionLibrary(config => config |
199 | | - .Indexer(get: s => new Version(2,0,0,0), set: (s, version) => {}) // Overwrites the indexer getter and setter |
200 | | - .Indexer(values: versions) // Provides a dictionary to retrieve and store versions |
201 | | - ); |
202 | | - |
203 | | - var preCurrent = versionLibrary["current"]; |
204 | | - versionLibrary["current"] = new Version(3, 0, 0, 0); |
205 | | - var postCurrent = versionLibrary["current"]; |
206 | | - Assert.NotEqual(preCurrent, postCurrent); |
207 | | -} |
208 | | -``` |
209 | | - |
210 | | -### Raising Events |
211 | | - |
212 | | -Raise events using an event trigger. |
213 | | - |
214 | | -```csharp |
215 | | -Action<Version>? triggerNewVersionAdded = null; |
216 | | - |
217 | | -var versionLibrary = Mock.IVersionLibrary(config => config |
218 | | - .NewVersionAdded(trigger: out triggerNewVersionAdded) // Provides a trigger for when a new version is added |
219 | | -); |
220 | | - |
221 | | -triggerNewVersionAdded?.Invoke(new Version(2, 0, 0, 0)); |
222 | | -``` |
223 | | - |
224 | | -### Argument Matching |
225 | | - |
226 | | -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. |
227 | | - |
228 | | -```csharp |
229 | | -var versionLibrary = Mock.IVersionLibrary(config => config |
230 | | - .DownloadExists(call: version => version is { Major: 2, Minor: 0 }) // Returns true for version 2.0.x based on a version parameter |
231 | | -); |
| 14 | + [Fact] |
| 15 | + [Mock<IBookRepository>] |
| 16 | + public async Task BookCanBeCreated() |
| 17 | + { |
| 18 | + Action<Book> trigger = _ => { }; |
| 19 | + |
| 20 | + var mockRepo = Mock.IBookRepository(config => config |
| 21 | + .AddBook(returns: Guid.NewGuid()) |
| 22 | + .BookCount(value: 10) |
| 23 | + .Indexer(values: new Dictionary<Guid, Book>()) |
| 24 | + .NewBookAdded(trigger: out trigger)); |
| 25 | + |
| 26 | + var sut = new BookModel(mockRepo); |
| 27 | + var actual = await sut.AddBook(new Book()); |
| 28 | + |
| 29 | + Assert.Equal("We now have 10 books", actual); |
| 30 | + } |
232 | 31 | ``` |
233 | 32 |
|
234 | | -__Using Internal Functions__ |
| 33 | +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. |
235 | 34 |
|
236 | | -```csharp |
237 | | -var versionLibrary = Mock.IVersionLibrary(config => |
238 | | -{ |
239 | | - bool downloadExists(Version version) => version switch { |
240 | | - { Major: 1, Minor: 0 } => true, // Returns true for version 1.0.x based on a version parameter |
241 | | - { Major: 2, Minor: 0, Revision: 0 } => true, // Returns true for version 2.0.0.0 based on a version parameter |
242 | | - { Major: 3, } => false, // Returns false for version 3.x based on a version parameter |
243 | | - _ => throw new ArgumentException() // Throws an exception for all other versions |
244 | | - }; |
| 35 | +For more details on specific aspects you can read about [Methods](guide/methods.md), [Properties](guide/properties.md), [Events](guide/events.md) or |
| 36 | +[Indexers](guide/indexers.md). |
| 37 | +Or you shift into high gear with [Advanced Features](advanced-features.md), [FaQ](faq.md) or [Troubleshooting](troubleshooting.md). |
245 | 38 |
|
246 | | - config.DownloadExists(downloadExists); |
247 | | -}); |
248 | | -``` |
| 39 | +If you are more into the ins and outs of MiniMock you can read the [Motivation](motivation.md) behind MiniMock or the [ADRs](../ADR/README.md). |
0 commit comments