Skip to content
Merged
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
190 changes: 190 additions & 0 deletions TUnit.Assertions/Conditions/Wrappers/CountWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
using System.Collections;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using TUnit.Assertions.Conditions;
using TUnit.Assertions.Core;
using TUnit.Assertions.Extensions;

namespace TUnit.Assertions.Conditions.Wrappers;

/// <summary>
/// Wrapper for collection count assertions that provides .EqualTo() method.
/// Example: await Assert.That(list).Count().EqualTo(5);
/// </summary>
public class CountWrapper<TCollection, TItem> : IAssertionSource<TCollection>
where TCollection : IEnumerable<TItem>
{
private readonly AssertionContext<TCollection> _context;

public CountWrapper(AssertionContext<TCollection> context)
{
_context = context;
}

AssertionContext<TCollection> IAssertionSource<TCollection>.Context => _context;

private static int GetCount(TCollection? value) =>
value is null ? 0
: value is ICollection collection ? collection.Count
: value.Cast<object>().Count();

private AssertionContext<int> MapToCount() => _context.Map<int>(GetCount);

/// <summary>
/// Not supported on CountWrapper - use IsTypeOf on the assertion source before calling HasCount().
/// </summary>
TypeOfAssertion<TCollection, TExpected> IAssertionSource<TCollection>.IsTypeOf<TExpected>()
{
throw new NotSupportedException(
"IsTypeOf is not supported after HasCount(). " +
"Use: Assert.That(value).IsTypeOf<List<int>>().HasCount().EqualTo(5)");
}

/// <summary>
/// Not supported on CountWrapper - use IsAssignableTo on the assertion source before calling HasCount().
/// </summary>
IsAssignableToAssertion<TTarget, TCollection> IAssertionSource<TCollection>.IsAssignableTo<TTarget>()
{
throw new NotSupportedException(
"IsAssignableTo is not supported after HasCount(). " +
"Use: Assert.That(value).IsAssignableTo<IList<int>>().HasCount().EqualTo(5)");
}

/// <summary>
/// Not supported on CountWrapper - use IsNotAssignableTo on the assertion source before calling HasCount().
/// </summary>
IsNotAssignableToAssertion<TTarget, TCollection> IAssertionSource<TCollection>.IsNotAssignableTo<TTarget>()
{
throw new NotSupportedException(
"IsNotAssignableTo is not supported after HasCount(). " +
"Use: Assert.That(value).IsNotAssignableTo<IList<int>>().HasCount().EqualTo(5)");
}

/// <summary>
/// Not supported on CountWrapper - use IsAssignableFrom on the assertion source before calling HasCount().
/// </summary>
IsAssignableFromAssertion<TTarget, TCollection> IAssertionSource<TCollection>.IsAssignableFrom<TTarget>()
{
throw new NotSupportedException(
"IsAssignableFrom is not supported after HasCount(). " +
"Use: Assert.That(value).IsAssignableFrom<IList<int>>().HasCount().EqualTo(5)");
}

/// <summary>
/// Not supported on CountWrapper - use IsNotAssignableFrom on the assertion source before calling HasCount().
/// </summary>
IsNotAssignableFromAssertion<TTarget, TCollection> IAssertionSource<TCollection>.IsNotAssignableFrom<TTarget>()
{
throw new NotSupportedException(
"IsNotAssignableFrom is not supported after HasCount(). " +
"Use: Assert.That(value).IsNotAssignableFrom<IList<int>>().HasCount().EqualTo(5)");
}

/// <summary>
/// Not supported on CountWrapper - use IsNotTypeOf on the assertion source before calling HasCount().
/// </summary>
IsNotTypeOfAssertion<TCollection, TExpected> IAssertionSource<TCollection>.IsNotTypeOf<TExpected>()
{
throw new NotSupportedException(
"IsNotTypeOf is not supported after HasCount(). " +
"Use: Assert.That(value).IsNotTypeOf<List<int>>().HasCount().EqualTo(5)");
}

/// <summary>
/// Asserts that the collection count is equal to the expected count.
/// </summary>
public CollectionCountAssertion<TCollection, TItem> EqualTo(
int expectedCount,
[CallerArgumentExpression(nameof(expectedCount))] string? expression = null)
{
_context.ExpressionBuilder.Append($".EqualTo({expression})");
return new CollectionCountAssertion<TCollection, TItem>(_context, expectedCount);
}

/// <summary>
/// Asserts that the collection count is greater than or equal to the expected count.
/// </summary>
public TValue_IsGreaterThanOrEqualTo_TValue_Assertion<int> GreaterThanOrEqualTo(
int expected,
[CallerArgumentExpression(nameof(expected))] string? expression = null)
{
_context.ExpressionBuilder.Append($".GreaterThanOrEqualTo({expression})");
return new TValue_IsGreaterThanOrEqualTo_TValue_Assertion<int>(MapToCount(), expected);
}

/// <summary>
/// Asserts that the collection count is positive (greater than 0).
/// </summary>
public TValue_IsGreaterThan_TValue_Assertion<int> Positive()
{
_context.ExpressionBuilder.Append(".Positive()");
return new TValue_IsGreaterThan_TValue_Assertion<int>(MapToCount(), 0);
}

/// <summary>
/// Asserts that the collection count is greater than the expected count.
/// </summary>
public TValue_IsGreaterThan_TValue_Assertion<int> GreaterThan(
int expected,
[CallerArgumentExpression(nameof(expected))] string? expression = null)
{
_context.ExpressionBuilder.Append($".GreaterThan({expression})");
return new TValue_IsGreaterThan_TValue_Assertion<int>(MapToCount(), expected);
}

/// <summary>
/// Asserts that the collection count is less than the expected count.
/// </summary>
public TValue_IsLessThan_TValue_Assertion<int> LessThan(
int expected,
[CallerArgumentExpression(nameof(expected))] string? expression = null)
{
_context.ExpressionBuilder.Append($".LessThan({expression})");
return new TValue_IsLessThan_TValue_Assertion<int>(MapToCount(), expected);
}

/// <summary>
/// Asserts that the collection count is less than or equal to the expected count.
/// </summary>
public TValue_IsLessThanOrEqualTo_TValue_Assertion<int> LessThanOrEqualTo(
int expected,
[CallerArgumentExpression(nameof(expected))] string? expression = null)
{
_context.ExpressionBuilder.Append($".LessThanOrEqualTo({expression})");
return new TValue_IsLessThanOrEqualTo_TValue_Assertion<int>(MapToCount(), expected);
}

/// <summary>
/// Asserts that the collection count is between the minimum and maximum values.
/// </summary>
public BetweenAssertion<int> Between(
int minimum,
int maximum,
[CallerArgumentExpression(nameof(minimum))] string? minExpression = null,
[CallerArgumentExpression(nameof(maximum))] string? maxExpression = null)
{
_context.ExpressionBuilder.Append($".Between({minExpression}, {maxExpression})");
return new BetweenAssertion<int>(MapToCount(), minimum, maximum);
}

/// <summary>
/// Asserts that the collection count is zero (empty collection).
/// </summary>
public CollectionCountAssertion<TCollection, TItem> Zero()
{
_context.ExpressionBuilder.Append(".Zero()");
return new CollectionCountAssertion<TCollection, TItem>(_context, 0);
}

/// <summary>
/// Asserts that the collection count is not equal to the expected count.
/// </summary>
public NotEqualsAssertion<int> NotEqualTo(
int expected,
[CallerArgumentExpression(nameof(expected))] string? expression = null)
{
_context.ExpressionBuilder.Append($".NotEqualTo({expression})");
return new NotEqualsAssertion<int>(MapToCount(), expected);
}
}
93 changes: 93 additions & 0 deletions TUnit.Assertions/Conditions/Wrappers/LengthWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Runtime.CompilerServices;
using System.Text;
using TUnit.Assertions.Conditions;
using TUnit.Assertions.Core;

namespace TUnit.Assertions.Conditions.Wrappers;

/// <summary>
/// Wrapper for string length assertions that provides .EqualTo() method.
/// Example: await Assert.That(str).HasLength().EqualTo(5);
/// </summary>
public class LengthWrapper : IAssertionSource<string>
{
private readonly AssertionContext<string> _context;

public LengthWrapper(AssertionContext<string> context)
{
_context = context;
}

AssertionContext<string> IAssertionSource<string>.Context => _context;

/// <summary>
/// Not supported on LengthWrapper - use IsTypeOf on the assertion source before calling HasLength().
/// </summary>
TypeOfAssertion<string, TExpected> IAssertionSource<string>.IsTypeOf<TExpected>()
{
throw new NotSupportedException(
"IsTypeOf is not supported after HasLength(). " +
"Use: Assert.That(value).IsTypeOf<string>().HasLength().EqualTo(5)");
}

/// <summary>
/// Not supported on LengthWrapper - use IsAssignableTo on the assertion source before calling HasLength().
/// </summary>
IsAssignableToAssertion<TTarget, string> IAssertionSource<string>.IsAssignableTo<TTarget>()
{
throw new NotSupportedException(
"IsAssignableTo is not supported after HasLength(). " +
"Use: Assert.That(value).IsAssignableTo<string>().HasLength().EqualTo(5)");
}

/// <summary>
/// Not supported on LengthWrapper - use IsNotAssignableTo on the assertion source before calling HasLength().
/// </summary>
IsNotAssignableToAssertion<TTarget, string> IAssertionSource<string>.IsNotAssignableTo<TTarget>()
{
throw new NotSupportedException(
"IsNotAssignableTo is not supported after HasLength(). " +
"Use: Assert.That(value).IsNotAssignableTo<string>().HasLength().EqualTo(5)");
}

/// <summary>
/// Not supported on LengthWrapper - use IsAssignableFrom on the assertion source before calling HasLength().
/// </summary>
IsAssignableFromAssertion<TTarget, string> IAssertionSource<string>.IsAssignableFrom<TTarget>()
{
throw new NotSupportedException(
"IsAssignableFrom is not supported after HasLength(). " +
"Use: Assert.That(value).IsAssignableFrom<string>().HasLength().EqualTo(5)");
}

/// <summary>
/// Not supported on LengthWrapper - use IsNotAssignableFrom on the assertion source before calling HasLength().
/// </summary>
IsNotAssignableFromAssertion<TTarget, string> IAssertionSource<string>.IsNotAssignableFrom<TTarget>()
{
throw new NotSupportedException(
"IsNotAssignableFrom is not supported after HasLength(). " +
"Use: Assert.That(value).IsNotAssignableFrom<string>().HasLength().EqualTo(5)");
}

/// <summary>
/// Not supported on LengthWrapper - use IsNotTypeOf on the assertion source before calling HasLength().
/// </summary>
IsNotTypeOfAssertion<string, TExpected> IAssertionSource<string>.IsNotTypeOf<TExpected>()
{
throw new NotSupportedException(
"IsNotTypeOf is not supported after HasLength(). " +
"Use: Assert.That(value).IsNotTypeOf<string>().HasLength().EqualTo(5)");
}

/// <summary>
/// Asserts that the string length is equal to the expected length.
/// </summary>
public StringLengthAssertion EqualTo(
int expectedLength,
[CallerArgumentExpression(nameof(expectedLength))] string? expression = null)
{
_context.ExpressionBuilder.Append($".EqualTo({expression})");
return new StringLengthAssertion(_context, expectedLength);
}
}
27 changes: 27 additions & 0 deletions TUnit.Assertions/Extensions/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Text.RegularExpressions;
using TUnit.Assertions.Chaining;
using TUnit.Assertions.Conditions;
using TUnit.Assertions.Conditions.Wrappers;
using TUnit.Assertions.Core;
using TUnit.Assertions.Sources;

Expand Down Expand Up @@ -881,6 +882,32 @@ public static StringLengthWithInlineAssertionAssertion Length(
return new StringLengthWithInlineAssertionAssertion(source.Context, lengthAssertion);
}

/// <summary>
/// Returns a wrapper for string length assertions.
/// Example: await Assert.That(str).HasLength().EqualTo(5);
/// </summary>
[Obsolete("Use Length() instead, which provides all numeric assertion methods. Example: Assert.That(str).Length().IsGreaterThan(5)")]
public static LengthWrapper HasLength(
this IAssertionSource<string> source)
{
source.Context.ExpressionBuilder.Append(".HasLength()");
return new LengthWrapper(source.Context);
}

/// <summary>
/// Asserts that the string has the expected length.
/// Example: await Assert.That(str).HasLength(5);
/// </summary>
[Obsolete("Use Length().IsEqualTo(expectedLength) instead.")]
public static StringLengthAssertion HasLength(
this IAssertionSource<string> source,
int expectedLength,
[CallerArgumentExpression(nameof(expectedLength))] string? expression = null)
{
source.Context.ExpressionBuilder.Append($".HasLength({expression})");
return new StringLengthAssertion(source.Context, expectedLength);
}

/// <summary>
/// Asserts that the value is structurally equivalent to the expected value.
/// Performs deep comparison of properties and fields.
Expand Down
27 changes: 27 additions & 0 deletions TUnit.Assertions/Sources/CollectionAssertionBase.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections;
using System.Runtime.CompilerServices;
using TUnit.Assertions.Conditions;
using TUnit.Assertions.Conditions.Wrappers;
using TUnit.Assertions.Core;

namespace TUnit.Assertions.Sources;
Expand Down Expand Up @@ -299,6 +300,32 @@ public CollectionHasAtMostAssertion<TCollection, TItem> HasAtMost(
return new CollectionHasAtMostAssertion<TCollection, TItem>(Context, maxCount);
}

/// <summary>
/// Asserts that the collection has the specified number of items.
/// This instance method enables calling HasCount with proper type inference.
/// Example: await Assert.That(list).HasCount(5);
/// </summary>
[Obsolete("Use Count().IsEqualTo(expectedCount) instead.")]
public CollectionCountAssertion<TCollection, TItem> HasCount(
int expectedCount,
[CallerArgumentExpression(nameof(expectedCount))] string? expression = null)
{
Context.ExpressionBuilder.Append($".HasCount({expression})");
return new CollectionCountAssertion<TCollection, TItem>(Context, expectedCount);
}

/// <summary>
/// Returns a wrapper for fluent count assertions.
/// This enables the pattern: .HasCount().GreaterThan(5)
/// Example: await Assert.That(list).HasCount().EqualTo(5);
/// </summary>
[Obsolete("Use Count() instead, which provides all numeric assertion methods. Example: Assert.That(list).Count().IsGreaterThan(5)")]
public CountWrapper<TCollection, TItem> HasCount()
{
Context.ExpressionBuilder.Append(".HasCount()");
return new CountWrapper<TCollection, TItem>(Context);
}

/// <summary>
/// Asserts that the collection count is between the specified minimum and maximum (inclusive).
/// This instance method enables calling HasCountBetween with proper type inference.
Expand Down
4 changes: 4 additions & 0 deletions TUnit.Core/Contexts/TestRegisteredContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public TestRegisteredContext(TestContext testContext)
/// </summary>
public ConcurrentDictionary<string, object?> StateBag => TestContext.StateBag.Items;

/// <inheritdoc cref="StateBag"/>
[Obsolete("Use StateBag property instead.")]
public ConcurrentDictionary<string, object?> ObjectBag => StateBag;

/// <summary>
/// Gets the test details from the underlying TestContext
/// </summary>
Expand Down
Loading
Loading