diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs index 1264810181..c663dd0307 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs @@ -135,6 +135,49 @@ public static T ContainsSingle(IEnumerable collection, string? message = " return default; } + /// + /// Tests whether the specified collection contains exactly one element. + /// + /// The collection. + /// The message to display when the assertion fails. + /// + /// The syntactic expression of collection as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// The item. + public static object ContainsSingle(IEnumerable collection, string? message = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") + { + int actualCount = 0; + object? firstItem = null; + + foreach (object? item in collection) + { + if (actualCount == 0) + { + firstItem = item; + } + + actualCount++; + + // Early exit if we already know there's more than one item + if (actualCount > 1) + { + break; + } + } + + if (actualCount == 1) + { + return firstItem!; + } + + string userMessage = BuildUserMessageForCollectionExpression(message, collectionExpression); + ThrowAssertContainsSingleFailed(actualCount, userMessage); + + // Unreachable code but compiler cannot work it out + return default!; + } + /// /// Tests whether the specified collection contains exactly one element that matches the given predicate. /// @@ -168,6 +211,52 @@ public static T ContainsSingle(Func predicate, IEnumerable collec return default; } + /// + /// Tests whether the specified collection contains exactly one element that matches the given predicate. + /// + /// A function to test each element for a condition. + /// The collection. + /// The message to display when the assertion fails. + /// + /// The syntactic expression of predicate as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of collection as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// The item that matches the predicate. + public static object ContainsSingle(Func predicate, IEnumerable collection, string? message = "", [CallerArgumentExpression(nameof(predicate))] string predicateExpression = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") + { + var matchingElements = new List(); + + foreach (object? item in collection) + { + if (predicate(item)) + { + matchingElements.Add(item); + + // Early exit optimization - no need to continue if we already have more than one match + if (matchingElements.Count > 1) + { + break; + } + } + } + + int actualCount = matchingElements.Count; + if (actualCount == 1) + { + return matchingElements[0]; + } + + string userMessage = BuildUserMessageForPredicateExpressionAndCollectionExpression(message, predicateExpression, collectionExpression); + ThrowAssertSingleMatchFailed(actualCount, userMessage); + + // Unreachable code but compiler cannot work it out + return default!; + } + #endregion // ContainsSingle #region Contains diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt index 7dc5c58110..0a398540b2 100644 --- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ContainsSingle(System.Collections.IEnumerable! collection, string? message = "", string! collectionExpression = "") -> object! +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ContainsSingle(System.Func! predicate, System.Collections.IEnumerable! collection, string? message = "", string! predicateExpression = "", string! collectionExpression = "") -> object! diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Contains.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Contains.cs index 39b830c8cd..c49b23ba27 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Contains.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Contains.cs @@ -279,6 +279,21 @@ public void ContainsSingle_NoMessage_WithSingleElement_ReturnsElement() result.Should().Be(100); } + /// + /// Tests the ContainsSingle method without message parameters where the collection has a single element. + /// + public void ContainsSingle_InNonGenericCollection_NoMessage_WithSingleElement_ReturnsElement() + { + // Arrange + var collection = new ArrayList { 100 }; + + // Act + object? result = Assert.ContainsSingle(collection); + + // Assert + result.Should().Be(100); + } + /// /// Tests the ContainsSingle method with a message where the collection has a single element. /// @@ -294,6 +309,21 @@ public void ContainsSingle_WithMessage_WithSingleElement_ReturnsElement() result.Should().Be("OnlyOne"); } + /// + /// Tests the ContainsSingle method with a message where the collection has a single element. + /// + public void ContainsSingle_InNonGenericCollection_WithMessage_WithSingleElement_ReturnsElement() + { + // Arrange + var collection = new ArrayList { "OnlyOne" }; + + // Act + object? result = Assert.ContainsSingle(collection, "Custom message"); + + // Assert + result.Should().Be("OnlyOne"); + } + /// /// Tests the ContainsSingle method that uses an interpolated string handler when the collection has multiple elements. /// Expects an exception. @@ -311,6 +341,50 @@ public void ContainsSingle_InterpolatedHandler_WithMultipleElements_ThrowsExcept action.Should().Throw().WithMessage("Assert.ContainsSingle failed. Expected collection to contain exactly one element but found 3 element(s). 'collection' expression: 'collection'. "); } + /// + /// Tests the ContainsSingle method without message parameters where the collection has a single element. + /// + public void ContainsSingle_InNonGenericCollection_NoMessage_WithNull_ReturnsElement() + { + // Arrange + var collection = new ArrayList { null }; + + // Act + object? result = Assert.ContainsSingle(collection); + + // Assert + result.Should().Be(null); + } + + /// + /// Tests the ContainsSingle method without message parameters where the collection has a single element. + /// + public void ContainsSingle_InNonGenericCollection_NoMessage_WithEmptyCollection_ReturnsNoElement() + { + // Arrange + var collection = new ArrayList(); + + // Act + Action action = () => Assert.ContainsSingle(collection); + + // Assert + action.Should().Throw(); + } + + /// + /// Tests the ContainsSingle method without message parameters where the collection has a no element (empty collection). + /// + public void ContainsSingle_InNonGenericCollection_AssertCustomMessage_WithEmptyCollection_ThrowsException() + { + // Arrange + var collection = new ArrayList(); + + // Act + Action action = () => Assert.ContainsSingle(collection); + + // Assert + action.Should().Throw().WithMessage("Assert.ContainsSingle failed. Expected collection to contain exactly one element but found 0 element(s). 'collection' expression: 'collection'."); + } #endregion #region Contains Tests