Skip to content

Commit

Permalink
Merge pull request #663 from Socolin/throws-async
Browse files Browse the repository at this point in the history
Add `.ThrowsAsync()` that will correctly mock exception on async methods
  • Loading branch information
dtchepak authored Feb 27, 2022
2 parents 992f2d8 + fe6e64d commit a6ce8c1
Show file tree
Hide file tree
Showing 4 changed files with 547 additions and 4 deletions.
11 changes: 7 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
### unreleased
* [NEW] Add `.ThrowsAsync()` that will correctly mock exception on async methods. (#609)

### 4.3.0 (Jan 2021)

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

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

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

### 2.0.2 (February 2017)
Expand Down
195 changes: 195 additions & 0 deletions src/NSubstitute/Extensions/ExceptionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using NSubstitute.Core;

// Disable nullability for client API, so it does not affect clients.
Expand Down Expand Up @@ -67,5 +70,197 @@ public static ConfiguredCall ThrowsForAnyArgs<TException>(this object value)
/// <returns></returns>
public static ConfiguredCall ThrowsForAnyArgs(this object value, Func<CallInfo, Exception> createException) =>
value.ReturnsForAnyArgs(ci => throw createException(ci));

/// <summary>
/// Throw an exception for this call.
/// </summary>
/// <param name="value"></param>
/// <param name="ex">Exception to throw</param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsync(this Task value, Exception ex) =>
value.Returns(_ => TaskFromException(ex));

/// <summary>
/// Throw an exception for this call.
/// </summary>
/// <param name="value"></param>
/// <param name="ex">Exception to throw</param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsync<T>(this Task<T> value, Exception ex) =>
value.Returns(_ => TaskFromException<T>(ex));

/// <summary>
/// Throw an exception of the given type for this call.
/// </summary>
/// <typeparam name="TException">Type of exception to throw</typeparam>
/// <param name="value"></param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsync<TException>(this Task value)
where TException : notnull, Exception, new()
{
return value.Returns(_ => FromException(value, new TException()));
}

/// <summary>
/// Throw an exception for this call, as generated by the specified function.
/// </summary>
/// <param name="value"></param>
/// <param name="createException">Func creating exception object</param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsync(this Task value, Func<CallInfo, Exception> createException) =>
value.Returns(ci => TaskFromException(createException(ci)));

/// <summary>
/// Throw an exception for this call, as generated by the specified function.
/// </summary>
/// <param name="value"></param>
/// <param name="createException">Func creating exception object</param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsync<T>(this Task<T> value, Func<CallInfo, Exception> createException) =>
value.Returns(ci => TaskFromException<T>(createException(ci)));

/// <summary>
/// Throw an exception for this call made with any arguments.
/// </summary>
/// <param name="value"></param>
/// <param name="ex">Exception to throw</param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsyncForAnyArgs(this Task value, Exception ex) =>
value.ReturnsForAnyArgs(_ => TaskFromException(ex));

/// <summary>
/// Throw an exception for this call made with any arguments.
/// </summary>
/// <param name="value"></param>
/// <param name="ex">Exception to throw</param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsyncForAnyArgs<T>(this Task<T> value, Exception ex) =>
value.ReturnsForAnyArgs(_ => TaskFromException<T>(ex));

/// <summary>
/// Throws an exception of the given type for this call made with any arguments.
/// </summary>
/// <typeparam name="TException">Type of exception to throw</typeparam>
/// <param name="value"></param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsyncForAnyArgs<TException>(this Task value)
where TException : notnull, Exception, new()
{
return value.ReturnsForAnyArgs(_ => FromException(value, new TException()));
}

/// <summary>
/// Throws an exception for this call made with any arguments, as generated by the specified function.
/// </summary>
/// <param name="value"></param>
/// <param name="createException">Func creating exception object</param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsyncForAnyArgs(this Task value, Func<CallInfo, Exception> createException) =>
value.ReturnsForAnyArgs(ci => TaskFromException(createException(ci)));

/// <summary>
/// Throws an exception for this call made with any arguments, as generated by the specified function.
/// </summary>
/// <param name="value"></param>
/// <param name="createException">Func creating exception object</param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsyncForAnyArgs<T>(this Task<T> value, Func<CallInfo, Exception> createException) =>
value.ReturnsForAnyArgs(ci => TaskFromException<T>(createException(ci)));

#if NET5_0_OR_GREATER
/// <summary>
/// Throw an exception for this call.
/// </summary>
/// <param name="value"></param>
/// <param name="ex">Exception to throw</param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsync<T>(this ValueTask<T> value, Exception ex) =>
value.Returns(_ => ValueTask.FromException<T>(ex));

/// <summary>
/// Throw an exception of the given type for this call.
/// </summary>
/// <typeparam name="TResult">Type of exception to throw</typeparam>
/// <typeparam name="TException">Type of exception to throw</typeparam>
/// <param name="value"></param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsync<TResult, TException>(this ValueTask<TResult> value)
where TException : notnull, Exception, new()
{
return value.Returns(_ => ValueTask.FromException<TResult>(new TException()));
}

/// <summary>
/// Throw an exception for this call, as generated by the specified function.
/// </summary>
/// <param name="value"></param>
/// <param name="createException">Func creating exception object</param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsync<T>(this ValueTask<T> value, Func<CallInfo, Exception> createException) =>
value.Returns(ci => ValueTask.FromException<T>(createException(ci)));

/// <summary>
/// Throws an exception of the given type for this call made with any arguments.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TException">Type of exception to throw</typeparam>
/// <param name="value"></param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsyncForAnyArgs<T, TException>(this ValueTask<T> value)
where TException : notnull, Exception, new()
{
return value.ReturnsForAnyArgs(_ => ValueTask.FromException<T>(new TException()));
}

/// <summary>
/// Throw an exception for this call made with any arguments.
/// </summary>
/// <param name="value"></param>
/// <param name="ex">Exception to throw</param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsyncForAnyArgs<T>(this ValueTask<T> value, Exception ex) =>
value.ReturnsForAnyArgs(_ => ValueTask.FromException<T>(ex));

/// <summary>
/// Throws an exception for this call made with any arguments, as generated by the specified function.
/// </summary>
/// <param name="value"></param>
/// <param name="createException">Func creating exception object</param>
/// <returns></returns>
public static ConfiguredCall ThrowsAsyncForAnyArgs<T>(this ValueTask<T> value, Func<CallInfo, Exception> createException) =>
value.ReturnsForAnyArgs(ci => ValueTask.FromException<T>(createException(ci)));
#endif

private static object FromException(object value, Exception exception)
{
// Handle Task<T>
var valueType = value.GetType();
if (valueType.IsConstructedGenericType)
{
var fromExceptionMethodInfo = typeof(Task).GetMethods(BindingFlags.Static | BindingFlags.Public).Single(m => m.Name == "FromException" && m.ContainsGenericParameters);
var specificFromExceptionMethod = fromExceptionMethodInfo.MakeGenericMethod(valueType.GenericTypeArguments);
return specificFromExceptionMethod.Invoke(null, new object[] {exception});
}

return TaskFromException(exception);
}

private static Task TaskFromException(Exception ex) {
#if NET45
return new Task(() => throw ex);
#else
return Task.FromException(ex);
#endif
}

private static Task<T> TaskFromException<T>(Exception ex)
{
#if NET45
return new Task<T>(() => throw ex);
#else
return Task<T>.FromException<T>(ex);
#endif
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ public interface ISomething

object this[string key] { get; set; }
System.Threading.Tasks.Task Async();
System.Threading.Tasks.Task DoAsync(object stuff);
System.Threading.Tasks.Task<int> CountAsync();
System.Threading.Tasks.Task<int> AnythingAsync(object stuff);
System.Threading.Tasks.Task<string> EchoAsync(int i);
System.Threading.Tasks.Task<string> SayAsync(string s);
System.Threading.Tasks.Task<SomeClass> SomeActionAsync();
System.Threading.Tasks.Task<SomeClass> SomeActionWithParamsAsync(int i, string s);

System.Threading.Tasks.ValueTask<int> CountValueTaskAsync();
System.Threading.Tasks.ValueTask<string> EchoValueTaskAsync(int i);
System.Threading.Tasks.ValueTask<int> AnythingValueTaskAsync(object stuff);
System.Threading.Tasks.ValueTask<string> SayValueTaskAsync(string s);
System.Threading.Tasks.ValueTask<SomeClass> SomeActionValueTaskAsync();
System.Threading.Tasks.ValueTask<SomeClass> SomeActionWithParamsValueTaskAsync(int i, string s);
Expand Down
Loading

0 comments on commit a6ce8c1

Please sign in to comment.