Skip to content

Commit

Permalink
Introduce Assert.Throws(Async) and Assert.ThrowsExactly(Async) APIs (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Evangelink authored Dec 14, 2024
1 parent 47cda79 commit b10bda3
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 21 deletions.
163 changes: 148 additions & 15 deletions src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

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

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

Expand All @@ -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>
Expand All @@ -22,14 +77,15 @@ 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"/>.
/// </exception>
/// <returns>
/// The exception that was thrown.
/// </returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public static T ThrowsException<T>(Action action)
where T : Exception
=> ThrowsException<T>(action, string.Empty, null);
Expand All @@ -55,6 +111,7 @@ public static T ThrowsException<T>(Action action)
/// <returns>
/// The exception that was thrown.
/// </returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public static T ThrowsException<T>(Action action, string message)
where T : Exception
=> ThrowsException<T>(action, message, null);
Expand All @@ -76,6 +133,7 @@ public static T ThrowsException<T>(Action action, string message)
/// <returns>
/// The exception that was thrown.
/// </returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public static T ThrowsException<T>(Func<object?> action)
where T : Exception
=> ThrowsException<T>(action, string.Empty, null);
Expand All @@ -101,6 +159,7 @@ public static T ThrowsException<T>(Func<object?> action)
/// <returns>
/// The exception that was thrown.
/// </returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public static T ThrowsException<T>(Func<object?> action, string message)
where T : Exception
=> ThrowsException<T>(action, message, null);
Expand Down Expand Up @@ -129,6 +188,7 @@ public static T ThrowsException<T>(Func<object?> action, string message)
/// <returns>
/// The exception that was thrown.
/// </returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public static T ThrowsException<T>(Func<object?> action, string message, params object?[]? parameters)
where T : Exception
#pragma warning disable IDE0053 // Use expression body for lambda expression
Expand Down Expand Up @@ -160,9 +220,13 @@ public static T ThrowsException<T>(Func<object?> action, string message, params
/// <returns>
/// The exception that was thrown.
/// </returns>
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and format appropriately.")]
[EditorBrowsable(EditorBrowsableState.Never)]
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);
Expand All @@ -174,33 +238,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>
Expand All @@ -218,6 +339,7 @@ public static T ThrowsException<T>(Action action, string message, params object?
/// <returns>
/// The <see cref="Task"/> executing the delegate.
/// </returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public static async Task<T> ThrowsExceptionAsync<T>(Func<Task> action)
where T : Exception
=> await ThrowsExceptionAsync<T>(action, string.Empty, null)
Expand All @@ -240,6 +362,7 @@ public static async Task<T> ThrowsExceptionAsync<T>(Func<Task> action)
/// <returns>
/// The <see cref="Task"/> executing the delegate.
/// </returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public static async Task<T> ThrowsExceptionAsync<T>(Func<Task> action, string message)
where T : Exception
=> await ThrowsExceptionAsync<T>(action, message, null)
Expand All @@ -265,8 +388,14 @@ public static async Task<T> ThrowsExceptionAsync<T>(Func<Task> action, string me
/// <returns>
/// The <see cref="Task"/> executing the delegate.
/// </returns>
[EditorBrowsable(EditorBrowsableState.Never)]
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);
Expand All @@ -278,28 +407,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!;
Expand Down
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!>!
Loading

0 comments on commit b10bda3

Please sign in to comment.