The other day I was writing a unit test to verify what was supposed to happen when a Task
would throw an exception when awaited. It wasn't as straight forward as I first assumed, but I quickly found this Stack Overflow post that helped me:
Normally, for synchronous calls, you can just add a .Throws<TException>()
after the method you want to mock throwing an exception (or .ThrowsForAnyArgs<TException>()
if you don't care about the input arguments). If you do this on a method returning a Task
then that call would instantly throw, which is - most often - not what happens; it's the work being done by the Task
throwing an exception, not the action of creating the Task
.
As you can read in the Stack Overflow post the way to simulate a Task
throwing an exception is to have the method return an already faulted Task
, which can be done (sort of easily) with the static method Task.FromException<TReturn>(Exception)
.
interface IService
{
Task<MyModel> GetAsync();
}
var service = Substitute.For<IService>();
service.GetAsync().Returns(Task.FromException<MyModel>(new ArgumentException("bla bla"));
But that's a bit too verbose for my taste, compared to this synchronous version:
var service = Substitute.For<IService>();
service.Get().Throws<ArgumentException>();
So I ended up creating a few extension methods enabling a more simple and readable syntax, so I could instead write it like this:
var service = Substitute.For<IService>();
// Any Exception
service.GetAsync().TaskThrowsException("optional message");
// Specific Exception v1
service.GetAsync().TaskThrows<MyModel, ArgumentException>("optional message");
// Specific Exception v2
service.GetAsync().TaskThrows(typeof(ArgumentException), "optional message");
// Specific Exception instance
service.GetAsync().TaskThrows(new ArgumentException());
That's a lot cleaner and more readable in my eyes :)
The TaskThrows<MyModel, ArgumentException>()
might seem a little weird, but for the generic Task<T>
we sadly can't have the generic arguments inferred as we can for the others :(
Here are all the extension methods for both Task
and Task<T>
:
public static class NSubstituteExtensions
{
public static ConfiguredCall TaskThrowsException<T>(this Task<T> task, string message = null)
{
return TaskThrows<T, Exception>(task, message);
}
public static ConfiguredCall TaskThrows<T, TException>(this Task<T> task, string message = null)
where TException : Exception
{
return TaskThrows(task, typeof(TException));
}
public static ConfiguredCall TaskThrows<T>(this Task<T> task, Type exceptionType, string message = null)
{
if (!typeof(Exception).IsAssignableFrom(exceptionType))
throw new ArgumentException($"Type has to be a subclass of System.Exception", nameof(exceptionType));
var exception = Activator.CreateInstance(exceptionType, message) as Exception;
return TaskThrows(task, exception);
}
public static ConfiguredCall TaskThrows<T>(this Task<T> task, Exception exception)
{
return task.Returns(Task.FromException<T>(exception));
}
public static ConfiguredCall TaskThrows<TException>(this Task task, string message = null)
where TException : Exception
{
return TaskThrows(task, typeof(TException));
}
public static ConfiguredCall TaskThrows(this Task task, Type exceptionType, string message = null)
{
if (!typeof(Exception).IsAssignableFrom(exceptionType))
throw new ArgumentException($"Type has to be a subclass of System.Exception", nameof(exceptionType));
var exception = Activator.CreateInstance(exceptionType, message) as Exception;
return TaskThrows(task, exception);
}
public static ConfiguredCall TaskThrows(this Task task, Exception exception)
{
return task.Returns(Task.FromException(exception));
}
}