Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
29 changes: 29 additions & 0 deletions src/TestFramework/TestFramework/Assertions/Assert.Scope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// A collection of helper classes to test various conditions within
/// unit tests. If the condition being tested is not met, an exception
/// is thrown.
/// </summary>
public sealed partial class Assert
{
/// <summary>
/// Creates a new assertion scope that collects assertion failures instead of throwing them immediately.
/// When the returned scope is disposed, all collected failures are thrown as a single <see cref="AssertFailedException"/>.
/// </summary>
/// <returns>An <see cref="IDisposable"/> representing the assertion scope.</returns>
/// <example>
/// <code>
/// using (Assert.Scope())
/// {
/// Assert.AreEqual(1, 2); // collected, not thrown
/// Assert.IsTrue(false); // collected, not thrown
/// }
/// // AssertFailedException is thrown here with all collected failures.
/// </code>
/// </example>
public static IDisposable Scope() => new AssertScope();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public API is quite limited, do we want to make it experimental still?

}
15 changes: 13 additions & 2 deletions src/TestFramework/TestFramework/Assertions/Assert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,19 @@ private Assert()
[DoesNotReturn]
[StackTraceHidden]
internal static void ThrowAssertFailed(string assertionName, string? message)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this PR and #2033, I think it'd be good to expose a "ReportFailure" method that could be used by devs extending MSTest assertions so they would benefits from the various features (launch debugger, soft assertions....). I am not doing it here because we can decide not to go with it for now.

=> throw new AssertFailedException(
string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message));
{
var assertionFailedException = new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message));
AssertScope? scope = AssertScope.Current;
if (scope is not null)
{
scope.AddError(assertionFailedException);
#pragma warning disable CS8763 // A method marked [DoesNotReturn] should not return.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Youssef1313 I don't know what we prefer but it feels better for me to say we continue to help compiler with the default behavior and we know we may have some FPs when code is used under assertion scope. It would otherwise be a breaking change for many people if we had to update all assertion APIs to no longer respect some of these DoesNotReturn compilation indication.

return;
#pragma warning restore CS8763 // A method marked [DoesNotReturn] should not return.
}

throw assertionFailedException;
}

/// <summary>
/// Builds the formatted message using the given user format message and parameters.
Expand Down
53 changes: 53 additions & 0 deletions src/TestFramework/TestFramework/Assertions/AssertScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Represents a scope in which assertion failures are collected instead of thrown immediately.
/// When the scope is disposed, all collected failures are thrown as a single <see cref="AssertFailedException"/>.
/// </summary>
internal sealed class AssertScope : IDisposable
{
private static readonly AsyncLocal<AssertScope?> CurrentScope = new();

private readonly List<AssertFailedException> _errors = [];
private readonly AssertScope? _previousScope;
private bool _disposed;

internal AssertScope()
{
_previousScope = CurrentScope.Value;
CurrentScope.Value = this;
}

/// <summary>
/// Gets the current active <see cref="AssertScope"/>, or <see langword="null"/> if no scope is active.
/// </summary>
internal static AssertScope? Current => CurrentScope.Value;

/// <summary>
/// Adds an assertion failure message to the current scope.
/// </summary>
/// <param name="error">The assertion failure message.</param>
internal void AddError(AssertFailedException error) => _errors.Add(error);

/// <inheritdoc/>
public void Dispose()
{
if (_disposed)
{
return;
}

_disposed = true;
CurrentScope.Value = _previousScope;

if (_errors.Count > 0)
{
throw new AssertFailedException(
string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertScopeFailure, _errors.Count),
new AggregateException(_errors));
}
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.Scope() -> System.IDisposable!
Original file line number Diff line number Diff line change
Expand Up @@ -393,4 +393,8 @@ Actual: {2}</value>
<data name="IsInRangeMaxValueMustBeGreaterThanOrEqualMinValue" xml:space="preserve">
<value>The maximum value must be greater than or equal to the minimum value.</value>
</data>
<data name="AssertScopeFailure" xml:space="preserve">
<value>{0} assertion(s) failed within the assert scope:</value>
<comment>{0} is the number of assertion failures collected in the scope.</comment>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Skutečnost: {2}</target>
<target state="translated">CollectionAssert.ReferenceEquals se nemá používat pro kontrolní výrazy. Místo toho použijte metody CollectionAssert nebo Assert.AreSame &amp; overloads.</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Tatsächlich: {2}</target>
<target state="translated">CollectionAssert.ReferenceEquals darf nicht für Assertions verwendet werden. Verwenden Sie stattdessen CollectionAssert-Methoden oder Assert.AreSame und Überladungen.</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Real: {2}</target>
<target state="translated">CollectionAssert.ReferenceEquals no se debe usar para las aserciones. Use los métodos CollectionAssert o las sobrecargas Assert.AreSame &amp; en su lugar.</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Réel : {2}</target>
<target state="translated">CollectionAssert.ReferenceEquals ne doit pas être utilisé pour les assertions. Utilisez plutôt les méthodes CollectionAssert ou Assert.AreSame &amp; overloads.</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Effettivo: {2}</target>
<target state="translated">Non è possibile usare CollectionAssert.ReferenceEquals per le asserzioni. In alternativa, usare i metodi CollectionAssert o Assert.AreSame e gli overload.</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Actual: {2}</source>
<target state="translated">アサーションには CollectionAssert.ReferenceEquals を使用しないでください。代わりに CollectionAssert メソッドまたは Assert.AreSame およびオーバーロードを使用してください。</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Actual: {2}</source>
<target state="translated">CollectionAssert.ReferenceEquals는 Assertions에 사용할 수 없습니다. 대신 CollectionAssert 메서드 또는 Assert.AreSame 및 오버로드를 사용하세요.</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Rzeczywiste: {2}</target>
<target state="translated">Element Assert.ReferenceEquals nie powinien być używany dla asercji. Zamiast tego użyj metod CollectionAssert lub Assert.AreSame oraz ich przeciążeń.</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Real: {2}</target>
<target state="translated">CollectionAssert.ReferenceEquals não deve ser usado com as Declarações. Em vez disso, use os métodos CollectionAssert ou Assert.AreSame e as sobrecargas.</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Actual: {2}</source>
<target state="translated">Нельзя использовать CollectionAssert.ReferenceEquals для Assertions. Вместо этого используйте методы CollectionAssert или Assert.AreSame и перегрузки.</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Gerçekte olan: {2}</target>
<target state="translated">CollectionAssert.ReferenceEquals, Onaylama için kullanılmamalı. Lütfen bunun yerine CollectionAssert yöntemlerini veya Assert.AreSame &amp; aşırı yüklemelerini kullanın.</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Actual: {2}</source>
<target state="translated">CollectionAssert.ReferenceEquals 不应用于断言。请改用 CollectionAssert 方法或 Assert.AreSame 和重载。</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ Actual: {2}</source>
<target state="translated">CollectionAssert.ReferenceEquals 不應使用於判斷提示。請改用 CollectionAssert 方法或 Assert.AreSame 及其多載。</target>
<note></note>
</trans-unit>
<trans-unit id="AssertScopeFailure">
<source>{0} assertion(s) failed within the assert scope:</source>
<target state="new">{0} assertion(s) failed within the assert scope:</target>
<note>{0} is the number of assertion failures collected in the scope.</note>
</trans-unit>
</body>
</file>
</xliff>
Loading