Skip to content

Commit a6ce8c1

Browse files
authored
Merge pull request #663 from Socolin/throws-async
Add `.ThrowsAsync()` that will correctly mock exception on async methods
2 parents 992f2d8 + fe6e64d commit a6ce8c1

File tree

4 files changed

+547
-4
lines changed

4 files changed

+547
-4
lines changed

CHANGELOG.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
### unreleased
2+
* [NEW] Add `.ThrowsAsync()` that will correctly mock exception on async methods. (#609)
3+
14
### 4.3.0 (Jan 2021)
25

36
* [NEW] Add .NET 5 (#636) and .NET 6 (#674) support. Thanks to @zvirja and @Havunen!
@@ -55,7 +58,7 @@ See [BreakingChanges.md](BreakingChanges.md) if you are still using pre-C#7. (#4
5558
* [NEW] `Configure()` extension in `NSubstitute.Extensions.ConfigurationExtensions` to
5659
ensure NSubstitute handles the next call as a configuration/specification. (#350, @zvirja)
5760
* [UPDATE] Performance improvements. (@zvirja)
58-
- `CallResults` performance optimisation
61+
- `CallResults` performance optimisation
5962
- Delegate proxy generation improvements (#362)
6063
- Minimise allocations and LINQ use on hot code paths (#390)
6164
- Optimise array allocation (#395)
@@ -67,7 +70,7 @@ This helps fix some problems with overlapping configurations. See #345 and
6770

6871
#### New and improved debugging, errors and error messages
6972

70-
* [NEW] Raise `CouldNotConfigureBaseMethodException` when trying to configure a call to
73+
* [NEW] Raise `CouldNotConfigureBaseMethodException` when trying to configure a call to
7174
call a base method that does not exist. (#429, @zvirja)
7275
* [NEW] Raise `RedundantArgumentMatcherException` if extra arg matchers are detected. This is
7376
a huge help for immediately identifying misconfigured tests. (@zvirja)
@@ -85,7 +88,7 @@ Including (but not limited to):
8588
* [FIX] Improved handling of virtual calls in constructors. (#423, @zvirja)
8689
* [NEW] Added a set of `When()` overloads to configure async methods without compilation warnings. (#468, @zvirja)
8790
* [FIX] Fixed potential for `ArgumentNullException` on finalizer thread. (#382, @zvirja)
88-
* [UPDATE] Now using Castle.Core 4.3.1+. We :heart: you Castle.Core! (Thanks for the
91+
* [UPDATE] Now using Castle.Core 4.3.1+. We :heart: you Castle.Core! (Thanks for the
8992
PR Alexandr Nikitin!)
9093
* [NEW] Expose `.Received(Quantity)` in `NSubstitute.ReceivedExtensions` namespace. Thanks to
9194
@firelizzard18 for this suggestion.
@@ -131,7 +134,7 @@ Ian Johnson (@ipjohnson) for this change. (#303)
131134
* [UPDATE] .NET Core build is now signed, which removes a warning when
132135
referencing .NET standard library. Thanks to Connie Yau (@conniey) for
133136
this change. (#302)
134-
* [FIX] Removed redundant .NET standard dependencies when referencing
137+
* [FIX] Removed redundant .NET standard dependencies when referencing
135138
NSubstitute as a .NET Framework library. Thanks to Alex Povar (@zvirja). (#295)
136139

137140
### 2.0.2 (February 2017)

src/NSubstitute/Extensions/ExceptionExtensions.cs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System;
2+
using System.Linq;
3+
using System.Reflection;
4+
using System.Threading.Tasks;
25
using NSubstitute.Core;
36

47
// Disable nullability for client API, so it does not affect clients.
@@ -67,5 +70,197 @@ public static ConfiguredCall ThrowsForAnyArgs<TException>(this object value)
6770
/// <returns></returns>
6871
public static ConfiguredCall ThrowsForAnyArgs(this object value, Func<CallInfo, Exception> createException) =>
6972
value.ReturnsForAnyArgs(ci => throw createException(ci));
73+
74+
/// <summary>
75+
/// Throw an exception for this call.
76+
/// </summary>
77+
/// <param name="value"></param>
78+
/// <param name="ex">Exception to throw</param>
79+
/// <returns></returns>
80+
public static ConfiguredCall ThrowsAsync(this Task value, Exception ex) =>
81+
value.Returns(_ => TaskFromException(ex));
82+
83+
/// <summary>
84+
/// Throw an exception for this call.
85+
/// </summary>
86+
/// <param name="value"></param>
87+
/// <param name="ex">Exception to throw</param>
88+
/// <returns></returns>
89+
public static ConfiguredCall ThrowsAsync<T>(this Task<T> value, Exception ex) =>
90+
value.Returns(_ => TaskFromException<T>(ex));
91+
92+
/// <summary>
93+
/// Throw an exception of the given type for this call.
94+
/// </summary>
95+
/// <typeparam name="TException">Type of exception to throw</typeparam>
96+
/// <param name="value"></param>
97+
/// <returns></returns>
98+
public static ConfiguredCall ThrowsAsync<TException>(this Task value)
99+
where TException : notnull, Exception, new()
100+
{
101+
return value.Returns(_ => FromException(value, new TException()));
102+
}
103+
104+
/// <summary>
105+
/// Throw an exception for this call, as generated by the specified function.
106+
/// </summary>
107+
/// <param name="value"></param>
108+
/// <param name="createException">Func creating exception object</param>
109+
/// <returns></returns>
110+
public static ConfiguredCall ThrowsAsync(this Task value, Func<CallInfo, Exception> createException) =>
111+
value.Returns(ci => TaskFromException(createException(ci)));
112+
113+
/// <summary>
114+
/// Throw an exception for this call, as generated by the specified function.
115+
/// </summary>
116+
/// <param name="value"></param>
117+
/// <param name="createException">Func creating exception object</param>
118+
/// <returns></returns>
119+
public static ConfiguredCall ThrowsAsync<T>(this Task<T> value, Func<CallInfo, Exception> createException) =>
120+
value.Returns(ci => TaskFromException<T>(createException(ci)));
121+
122+
/// <summary>
123+
/// Throw an exception for this call made with any arguments.
124+
/// </summary>
125+
/// <param name="value"></param>
126+
/// <param name="ex">Exception to throw</param>
127+
/// <returns></returns>
128+
public static ConfiguredCall ThrowsAsyncForAnyArgs(this Task value, Exception ex) =>
129+
value.ReturnsForAnyArgs(_ => TaskFromException(ex));
130+
131+
/// <summary>
132+
/// Throw an exception for this call made with any arguments.
133+
/// </summary>
134+
/// <param name="value"></param>
135+
/// <param name="ex">Exception to throw</param>
136+
/// <returns></returns>
137+
public static ConfiguredCall ThrowsAsyncForAnyArgs<T>(this Task<T> value, Exception ex) =>
138+
value.ReturnsForAnyArgs(_ => TaskFromException<T>(ex));
139+
140+
/// <summary>
141+
/// Throws an exception of the given type for this call made with any arguments.
142+
/// </summary>
143+
/// <typeparam name="TException">Type of exception to throw</typeparam>
144+
/// <param name="value"></param>
145+
/// <returns></returns>
146+
public static ConfiguredCall ThrowsAsyncForAnyArgs<TException>(this Task value)
147+
where TException : notnull, Exception, new()
148+
{
149+
return value.ReturnsForAnyArgs(_ => FromException(value, new TException()));
150+
}
151+
152+
/// <summary>
153+
/// Throws an exception for this call made with any arguments, as generated by the specified function.
154+
/// </summary>
155+
/// <param name="value"></param>
156+
/// <param name="createException">Func creating exception object</param>
157+
/// <returns></returns>
158+
public static ConfiguredCall ThrowsAsyncForAnyArgs(this Task value, Func<CallInfo, Exception> createException) =>
159+
value.ReturnsForAnyArgs(ci => TaskFromException(createException(ci)));
160+
161+
/// <summary>
162+
/// Throws an exception for this call made with any arguments, as generated by the specified function.
163+
/// </summary>
164+
/// <param name="value"></param>
165+
/// <param name="createException">Func creating exception object</param>
166+
/// <returns></returns>
167+
public static ConfiguredCall ThrowsAsyncForAnyArgs<T>(this Task<T> value, Func<CallInfo, Exception> createException) =>
168+
value.ReturnsForAnyArgs(ci => TaskFromException<T>(createException(ci)));
169+
170+
#if NET5_0_OR_GREATER
171+
/// <summary>
172+
/// Throw an exception for this call.
173+
/// </summary>
174+
/// <param name="value"></param>
175+
/// <param name="ex">Exception to throw</param>
176+
/// <returns></returns>
177+
public static ConfiguredCall ThrowsAsync<T>(this ValueTask<T> value, Exception ex) =>
178+
value.Returns(_ => ValueTask.FromException<T>(ex));
179+
180+
/// <summary>
181+
/// Throw an exception of the given type for this call.
182+
/// </summary>
183+
/// <typeparam name="TResult">Type of exception to throw</typeparam>
184+
/// <typeparam name="TException">Type of exception to throw</typeparam>
185+
/// <param name="value"></param>
186+
/// <returns></returns>
187+
public static ConfiguredCall ThrowsAsync<TResult, TException>(this ValueTask<TResult> value)
188+
where TException : notnull, Exception, new()
189+
{
190+
return value.Returns(_ => ValueTask.FromException<TResult>(new TException()));
191+
}
192+
193+
/// <summary>
194+
/// Throw an exception for this call, as generated by the specified function.
195+
/// </summary>
196+
/// <param name="value"></param>
197+
/// <param name="createException">Func creating exception object</param>
198+
/// <returns></returns>
199+
public static ConfiguredCall ThrowsAsync<T>(this ValueTask<T> value, Func<CallInfo, Exception> createException) =>
200+
value.Returns(ci => ValueTask.FromException<T>(createException(ci)));
201+
202+
/// <summary>
203+
/// Throws an exception of the given type for this call made with any arguments.
204+
/// </summary>
205+
/// <typeparam name="T"></typeparam>
206+
/// <typeparam name="TException">Type of exception to throw</typeparam>
207+
/// <param name="value"></param>
208+
/// <returns></returns>
209+
public static ConfiguredCall ThrowsAsyncForAnyArgs<T, TException>(this ValueTask<T> value)
210+
where TException : notnull, Exception, new()
211+
{
212+
return value.ReturnsForAnyArgs(_ => ValueTask.FromException<T>(new TException()));
213+
}
214+
215+
/// <summary>
216+
/// Throw an exception for this call made with any arguments.
217+
/// </summary>
218+
/// <param name="value"></param>
219+
/// <param name="ex">Exception to throw</param>
220+
/// <returns></returns>
221+
public static ConfiguredCall ThrowsAsyncForAnyArgs<T>(this ValueTask<T> value, Exception ex) =>
222+
value.ReturnsForAnyArgs(_ => ValueTask.FromException<T>(ex));
223+
224+
/// <summary>
225+
/// Throws an exception for this call made with any arguments, as generated by the specified function.
226+
/// </summary>
227+
/// <param name="value"></param>
228+
/// <param name="createException">Func creating exception object</param>
229+
/// <returns></returns>
230+
public static ConfiguredCall ThrowsAsyncForAnyArgs<T>(this ValueTask<T> value, Func<CallInfo, Exception> createException) =>
231+
value.ReturnsForAnyArgs(ci => ValueTask.FromException<T>(createException(ci)));
232+
#endif
233+
234+
private static object FromException(object value, Exception exception)
235+
{
236+
// Handle Task<T>
237+
var valueType = value.GetType();
238+
if (valueType.IsConstructedGenericType)
239+
{
240+
var fromExceptionMethodInfo = typeof(Task).GetMethods(BindingFlags.Static | BindingFlags.Public).Single(m => m.Name == "FromException" && m.ContainsGenericParameters);
241+
var specificFromExceptionMethod = fromExceptionMethodInfo.MakeGenericMethod(valueType.GenericTypeArguments);
242+
return specificFromExceptionMethod.Invoke(null, new object[] {exception});
243+
}
244+
245+
return TaskFromException(exception);
246+
}
247+
248+
private static Task TaskFromException(Exception ex) {
249+
#if NET45
250+
return new Task(() => throw ex);
251+
#else
252+
return Task.FromException(ex);
253+
#endif
254+
}
255+
256+
private static Task<T> TaskFromException<T>(Exception ex)
257+
{
258+
#if NET45
259+
return new Task<T>(() => throw ex);
260+
#else
261+
return Task<T>.FromException<T>(ex);
262+
#endif
263+
}
264+
70265
}
71266
}

tests/NSubstitute.Acceptance.Specs/Infrastructure/ISomething.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@ public interface ISomething
1919

2020
object this[string key] { get; set; }
2121
System.Threading.Tasks.Task Async();
22+
System.Threading.Tasks.Task DoAsync(object stuff);
2223
System.Threading.Tasks.Task<int> CountAsync();
24+
System.Threading.Tasks.Task<int> AnythingAsync(object stuff);
2325
System.Threading.Tasks.Task<string> EchoAsync(int i);
2426
System.Threading.Tasks.Task<string> SayAsync(string s);
2527
System.Threading.Tasks.Task<SomeClass> SomeActionAsync();
2628
System.Threading.Tasks.Task<SomeClass> SomeActionWithParamsAsync(int i, string s);
2729

2830
System.Threading.Tasks.ValueTask<int> CountValueTaskAsync();
2931
System.Threading.Tasks.ValueTask<string> EchoValueTaskAsync(int i);
32+
System.Threading.Tasks.ValueTask<int> AnythingValueTaskAsync(object stuff);
3033
System.Threading.Tasks.ValueTask<string> SayValueTaskAsync(string s);
3134
System.Threading.Tasks.ValueTask<SomeClass> SomeActionValueTaskAsync();
3235
System.Threading.Tasks.ValueTask<SomeClass> SomeActionWithParamsValueTaskAsync(int i, string s);

0 commit comments

Comments
 (0)