Skip to content

Introduce Assert.Throws(Async) and Assert.ThrowsExactly(Async) APIs #4350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 14, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 138 additions & 13 deletions src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

@@ -13,6 +14,60 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting;
/// </summary>
public sealed partial class Assert
{
/// <summary>
/// Asserts that the delegate <paramref name="action"/> throws an exception of type <typeparamref name="TException"/>
/// (or derived type) and throws <c>AssertFailedException</c> if code does not throws exception or throws
/// exception of type other than <typeparamref name="TException"/>.
/// </summary>
/// <param name="action">
/// Delegate to code to be tested and which is expected to throw exception.
/// </param>
/// <param name="message">
/// The message to include in the exception when <paramref name="action"/> does not throws exception of type <typeparamref name="TException"/>.
/// </param>
/// <param name="messageArgs">
/// An array of parameters to use when formatting <paramref name="message"/>.
/// </param>
/// <typeparam name="TException">
/// The type of exception expected to be thrown.
/// </typeparam>
/// <exception cref="AssertFailedException">
/// Thrown if <paramref name="action"/> does not throws exception of type <typeparamref name="TException"/>.
/// </exception>
/// <returns>
/// The exception that was thrown.
/// </returns>
public static TException Throws<TException>(Action action, string message = "", params object[] messageArgs)
where TException : Exception
=> ThrowsException<TException>(action, isStrictType: false, message, parameters: messageArgs);

/// <summary>
/// Asserts that the delegate <paramref name="action"/> throws an exception of type <typeparamref name="TException"/>
/// (and not of derived type) and throws <c>AssertFailedException</c> if code does not throws exception or throws
/// exception of type other than <typeparamref name="TException"/>.
/// </summary>
/// <param name="action">
/// Delegate to code to be tested and which is expected to throw exception.
/// </param>
/// <param name="message">
/// The message to include in the exception when <paramref name="action"/> does not throws exception of type <typeparamref name="TException"/>.
/// </param>
/// <param name="messageArgs">
/// An array of parameters to use when formatting <paramref name="message"/>.
/// </param>
/// <typeparam name="TException">
/// The type of exception expected to be thrown.
/// </typeparam>
/// <exception cref="AssertFailedException">
/// Thrown if <paramref name="action"/> does not throws exception of type <typeparamref name="TException"/>.
/// </exception>
/// <returns>
/// The exception that was thrown.
/// </returns>
public static TException ThrowsExactly<TException>(Action action, string message = "", params object[] messageArgs)
where TException : Exception
=> ThrowsException<TException>(action, isStrictType: true, message, parameters: messageArgs);

/// <summary>
/// Tests whether the code specified by delegate <paramref name="action"/> throws exact given exception
/// of type <typeparamref name="T"/> (and not of derived type) and throws <c>AssertFailedException</c>
@@ -22,7 +77,7 @@ public sealed partial class Assert
/// Delegate to code to be tested and which is expected to throw exception.
/// </param>
/// <typeparam name="T">
/// Type of exception expected to be thrown.
/// The exact type of exception expected to be thrown.
/// </typeparam>
/// <exception cref="AssertFailedException">
/// Thrown if <paramref name="action"/> does not throws exception of type <typeparamref name="T"/>.
@@ -163,6 +218,10 @@ public static T ThrowsException<T>(Func<object?> action, string message, params
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and format appropriately.")]
public static T ThrowsException<T>(Action action, string message, params object?[]? parameters)
where T : Exception
=> ThrowsException<T>(action, isStrictType: true, message, parameters: parameters);

private static TException ThrowsException<TException>(Action action, bool isStrictType, string message, [CallerMemberName] string assertMethodName = "", params object?[]? parameters)
where TException : Exception
{
Guard.NotNull(action);
Guard.NotNull(message);
@@ -174,33 +233,90 @@ public static T ThrowsException<T>(Action action, string message, params object?
}
catch (Exception ex)
{
if (!typeof(T).Equals(ex.GetType()))
bool isExceptionOfType = isStrictType
? typeof(TException) == ex.GetType()
: ex is TException;
if (!isExceptionOfType)
{
userMessage = BuildUserMessage(message, parameters);
finalMessage = string.Format(
CultureInfo.CurrentCulture,
FrameworkMessages.WrongExceptionThrown,
userMessage,
typeof(T),
typeof(TException),
ex.GetType());
ThrowAssertFailed("Assert.ThrowsException", finalMessage);
ThrowAssertFailed("Assert." + assertMethodName, finalMessage);
}

return (T)ex;
return (TException)ex;
}

userMessage = BuildUserMessage(message, parameters);
finalMessage = string.Format(
CultureInfo.CurrentCulture,
FrameworkMessages.NoExceptionThrown,
userMessage,
typeof(T));
ThrowAssertFailed("Assert.ThrowsException", finalMessage);
typeof(TException));
ThrowAssertFailed("Assert." + assertMethodName, finalMessage);

// This will not hit, but need it for compiler.
return null;
}

/// <summary>
/// Asserts that the delegate <paramref name="action"/> throws an exception of type <typeparamref name="TException"/>
/// (or derived type) and throws <c>AssertFailedException</c> if code does not throws exception or throws
/// exception of type other than <typeparamref name="TException"/>.
/// </summary>
/// <param name="action">
/// Delegate to code to be tested and which is expected to throw exception.
/// </param>
/// <param name="message">
/// The message to include in the exception when <paramref name="action"/> does not throws exception of type <typeparamref name="TException"/>.
/// </param>
/// <param name="messageArgs">
/// An array of parameters to use when formatting <paramref name="message"/>.
/// </param>
/// <typeparam name="TException">
/// The type of exception expected to be thrown.
/// </typeparam>
/// <exception cref="AssertFailedException">
/// Thrown if <paramref name="action"/> does not throws exception of type <typeparamref name="TException"/>.
/// </exception>
/// <returns>
/// The exception that was thrown.
/// </returns>
public static Task<TException> ThrowsAsync<TException>(Func<Task> action, string message = "", params object[] messageArgs)
where TException : Exception
=> ThrowsExceptionAsync<TException>(action, isStrictType: false, message, parameters: messageArgs);

/// <summary>
/// Asserts that the delegate <paramref name="action"/> throws an exception of type <typeparamref name="TException"/>
/// (and not of derived type) and throws <c>AssertFailedException</c> if code does not throws exception or throws
/// exception of type other than <typeparamref name="TException"/>.
/// </summary>
/// <param name="action">
/// Delegate to code to be tested and which is expected to throw exception.
/// </param>
/// <param name="message">
/// The message to include in the exception when <paramref name="action"/> does not throws exception of type <typeparamref name="TException"/>.
/// </param>
/// <param name="messageArgs">
/// An array of parameters to use when formatting <paramref name="message"/>.
/// </param>
/// <typeparam name="TException">
/// The type of exception expected to be thrown.
/// </typeparam>
/// <exception cref="AssertFailedException">
/// Thrown if <paramref name="action"/> does not throws exception of type <typeparamref name="TException"/>.
/// </exception>
/// <returns>
/// The exception that was thrown.
/// </returns>
public static Task<TException> ThrowsExactlyAsync<TException>(Func<Task> action, string message = "", params object[] messageArgs)
where TException : Exception
=> ThrowsExceptionAsync<TException>(action, isStrictType: true, message, parameters: messageArgs);

/// <summary>
/// Tests whether the code specified by delegate <paramref name="action"/> throws exact given exception
/// of type <typeparamref name="T"/> (and not of derived type) and throws <c>AssertFailedException</c>
@@ -267,6 +383,11 @@ public static async Task<T> ThrowsExceptionAsync<T>(Func<Task> action, string me
/// </returns>
public static async Task<T> ThrowsExceptionAsync<T>(Func<Task> action, string message, params object?[]? parameters)
where T : Exception
=> await ThrowsExceptionAsync<T>(action, true, message, parameters: parameters)
.ConfigureAwait(false);

private static async Task<TException> ThrowsExceptionAsync<TException>(Func<Task> action, bool isStrictType, string message, [CallerMemberName] string assertMethodName = "", params object?[]? parameters)
where TException : Exception
{
Guard.NotNull(action);
Guard.NotNull(message);
@@ -278,28 +399,32 @@ public static async Task<T> ThrowsExceptionAsync<T>(Func<Task> action, string me
}
catch (Exception ex)
{
if (!typeof(T).Equals(ex.GetType()))
bool isExceptionOfType = isStrictType
? typeof(TException) == ex.GetType()
: ex is TException;

if (!isExceptionOfType)
{
userMessage = BuildUserMessage(message, parameters);
finalMessage = string.Format(
CultureInfo.CurrentCulture,
FrameworkMessages.WrongExceptionThrown,
userMessage,
typeof(T),
typeof(TException),
ex.GetType());
ThrowAssertFailed("Assert.ThrowsException", finalMessage);
ThrowAssertFailed("Assert." + assertMethodName, finalMessage);
}

return (T)ex;
return (TException)ex;
}

userMessage = BuildUserMessage(message, parameters);
finalMessage = string.Format(
CultureInfo.CurrentCulture,
FrameworkMessages.NoExceptionThrown,
userMessage,
typeof(T));
ThrowAssertFailed("Assert.ThrowsException", finalMessage);
typeof(TException));
ThrowAssertFailed("Assert." + assertMethodName, finalMessage);

// This will not hit, but need it for compiler.
return null!;
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#nullable enable
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.Throws<TException>(System.Action! action, string! message = "", params object![]! messageArgs) -> TException!
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsAsync<TException>(System.Func<System.Threading.Tasks.Task!>! action, string! message = "", params object![]! messageArgs) -> System.Threading.Tasks.Task<TException!>!
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsExactly<TException>(System.Action! action, string! message = "", params object![]! messageArgs) -> TException!
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsExactlyAsync<TException>(System.Func<System.Threading.Tasks.Task!>! action, string! message = "", params object![]! messageArgs) -> System.Threading.Tasks.Task<TException!>!
Original file line number Diff line number Diff line change
@@ -72,7 +72,7 @@ public void ThrowsExceptionAsyncShouldThrowAssertionOnNoException()

Verify(innerException is not null);
Verify(typeof(AssertFailedException) == innerException.GetType());
Verify(innerException.Message.Equals("Assert.ThrowsException failed. Expected exception type:<System.ArgumentException> but no exception was thrown. ", StringComparison.Ordinal));
Verify(innerException.Message.Equals("Assert.ThrowsExceptionAsync failed. Expected exception type:<System.ArgumentException> but no exception was thrown. ", StringComparison.Ordinal));
}

public void ThrowsExceptionAsyncShouldThrowAssertionOnWrongException()
@@ -89,7 +89,7 @@ public void ThrowsExceptionAsyncShouldThrowAssertionOnWrongException()

Verify(innerException is not null);
Assert.AreEqual(typeof(AssertFailedException), innerException.GetType());
Verify(innerException.Message.Equals("Assert.ThrowsException failed. Expected exception type:<System.ArgumentException>. Actual exception type:<System.FormatException>. ", StringComparison.Ordinal));
Verify(innerException.Message.Equals("Assert.ThrowsExceptionAsync failed. Expected exception type:<System.ArgumentException>. Actual exception type:<System.FormatException>. ", StringComparison.Ordinal));
}

public void ThrowsExceptionAsyncWithMessageShouldThrowAssertionOnNoException()
@@ -103,7 +103,7 @@ public void ThrowsExceptionAsyncWithMessageShouldThrowAssertionOnNoException()

Verify(innerException is not null);
Assert.AreEqual(typeof(AssertFailedException), innerException.GetType());
Verify(innerException.Message.Equals("Assert.ThrowsException failed. Expected exception type:<System.ArgumentException> but no exception was thrown. The world is not on fire.", StringComparison.Ordinal));
Verify(innerException.Message.Equals("Assert.ThrowsExceptionAsync failed. Expected exception type:<System.ArgumentException> but no exception was thrown. The world is not on fire.", StringComparison.Ordinal));
}

public void ThrowsExceptionAsyncWithMessageShouldThrowAssertionOnWrongException()
@@ -121,7 +121,7 @@ public void ThrowsExceptionAsyncWithMessageShouldThrowAssertionOnWrongException(

Verify(innerException is not null);
Assert.AreEqual(typeof(AssertFailedException), innerException.GetType());
Verify(innerException.Message.Equals("Assert.ThrowsException failed. Expected exception type:<System.ArgumentException>. Actual exception type:<System.FormatException>. Happily ever after.", StringComparison.Ordinal));
Verify(innerException.Message.Equals("Assert.ThrowsExceptionAsync failed. Expected exception type:<System.ArgumentException>. Actual exception type:<System.FormatException>. Happily ever after.", StringComparison.Ordinal));
}

public void ThrowsExceptionAsyncWithMessageAndParamsShouldThrowOnNullAction()
@@ -170,7 +170,7 @@ public void ThrowsExceptionAsyncWithMessageAndParamsShouldThrowAssertionOnNoExce

Verify(innerException is not null);
Assert.AreEqual(typeof(AssertFailedException), innerException.GetType());
Verify(innerException.Message.Equals("Assert.ThrowsException failed. Expected exception type:<System.ArgumentException> but no exception was thrown. The world is not on fire ta.da-123.", StringComparison.Ordinal));
Verify(innerException.Message.Equals("Assert.ThrowsExceptionAsync failed. Expected exception type:<System.ArgumentException> but no exception was thrown. The world is not on fire ta.da-123.", StringComparison.Ordinal));
}

public void ThrowsExceptionAsyncWithMessageAndParamsShouldThrowAssertionOnWrongException()
@@ -190,7 +190,49 @@ public void ThrowsExceptionAsyncWithMessageAndParamsShouldThrowAssertionOnWrongE

Verify(innerException is not null);
Assert.AreEqual(typeof(AssertFailedException), innerException.GetType());
Verify(innerException.Message.Equals("Assert.ThrowsException failed. Expected exception type:<System.ArgumentException>. Actual exception type:<System.FormatException>. Happily ever after. The End.", StringComparison.Ordinal));
Verify(innerException.Message.Equals("Assert.ThrowsExceptionAsync failed. Expected exception type:<System.ArgumentException>. Actual exception type:<System.FormatException>. Happily ever after. The End.", StringComparison.Ordinal));
}
#endregion

public void Throws_WhenExceptionIsDerivedFromExpectedType_ShouldNotThrow()
=> Assert.Throws<ArgumentException>(() => throw new ArgumentNullException());

public void Throws_WhenExceptionIsNotExpectedType_ShouldThrow()
{
static void Action() => Assert.Throws<ArgumentException>(() => throw new Exception());
Exception ex = VerifyThrows(Action);
Verify(ex is AssertFailedException);
}

public void ThrowsExactly_WhenExceptionIsDerivedFromExpectedType_ShouldThrow()
{
static void Action() => Assert.ThrowsExactly<ArgumentException>(() => throw new ArgumentNullException());
Exception ex = VerifyThrows(Action);
Verify(ex is AssertFailedException);
}

public void ThrowsExactly_WhenExceptionExpectedType_ShouldNotThrow()
=> Assert.ThrowsExactly<ArgumentNullException>(() => throw new ArgumentNullException());

public async Task ThrowsAsync_WhenExceptionIsDerivedFromExpectedType_ShouldNotThrow()
=> await Assert.ThrowsAsync<ArgumentException>(() => throw new ArgumentNullException());

public void ThrowsAsync_WhenExceptionIsNotExpectedType_ShouldThrow()
{
Task t = Assert.ThrowsAsync<ArgumentException>(() => throw new Exception());
Exception ex = VerifyThrows(t.Wait);
Assert.IsInstanceOfType(ex.InnerException, out AssertFailedException assertFailedException);
Assert.AreEqual("Assert.ThrowsAsync failed. Expected exception type:<System.ArgumentException>. Actual exception type:<System.Exception>. ", assertFailedException.Message);
}

public void ThrowsExactlyAsync_WhenExceptionIsDerivedFromExpectedType_ShouldThrow()
{
Task t = Assert.ThrowsExactlyAsync<ArgumentException>(() => throw new ArgumentNullException());
Exception ex = VerifyThrows(t.Wait);
Assert.IsInstanceOfType(ex.InnerException, out AssertFailedException assertFailedException);
Assert.AreEqual("Assert.ThrowsExactlyAsync failed. Expected exception type:<System.ArgumentException>. Actual exception type:<System.ArgumentNullException>. ", assertFailedException.Message);
}

public async Task ThrowsExactlyAsync_WhenExceptionExpectedType_ShouldNotThrow()
=> await Assert.ThrowsExactlyAsync<ArgumentNullException>(() => throw new ArgumentNullException());
}