Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b9dd82d
+semver:minor - refactor(assertions): enhance collection assertion ty…
thomhurst Oct 16, 2025
605c4e6
Merge branch 'main' into bug/3401
thomhurst Oct 16, 2025
4618371
refactor(assertions): implement ComparerBasedAssertion and ToleranceB…
thomhurst Oct 16, 2025
2913390
refactor(assertions): simplify assertion linking and enhance type saf…
thomhurst Oct 16, 2025
47d3282
refactor(assertions): introduce ICollectionAssertionSource for enhanc…
thomhurst Oct 16, 2025
d1bf905
refactor(assertions): introduce ContinuationBase for common logic in …
thomhurst Oct 17, 2025
6a791d9
Merge branch 'main' into bug/3401
thomhurst Oct 17, 2025
0c30e20
feat(assertions): introduce dictionary assertions with And/Or chainin…
thomhurst Oct 17, 2025
a6cb4b3
Merge branch 'main' into bug/3401
thomhurst Oct 17, 2025
ca06375
refactor(assertions): enhance collection assertion classes for better…
thomhurst Oct 17, 2025
698e264
Merge branch 'main' into bug/3401
thomhurst Oct 17, 2025
24c081c
feat(assertions): add collection assertion methods for improved type …
thomhurst Oct 17, 2025
0653ee6
Merge branch 'main' into bug/3401
thomhurst Oct 18, 2025
5ed43c1
fix(assertions): update assertions to use IsNotEmpty and HasCount for…
thomhurst Oct 18, 2025
0f7c3a5
fix(assertions): streamline HasCount assertions for clarity and consi…
thomhurst Oct 18, 2025
6dce633
fix(tests): improve assertions in DictionaryCollectionTests for clari…
thomhurst Oct 18, 2025
24b4f52
fix(tests): add assertions for CustomCollection and enhance collectio…
thomhurst Oct 18, 2025
43986aa
fix(assertions): simplify collection assertions to single type parame…
thomhurst Oct 20, 2025
cfd219a
feat(assertions): add HasCount method for fluent count assertions on …
thomhurst Oct 20, 2025
4452dbb
feat(assertions): add MapAsync method for asynchronous value transfor…
thomhurst Oct 20, 2025
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
Prev Previous commit
Next Next commit
refactor(assertions): introduce ContinuationBase for common logic in …
…And/Or continuations and enhance ICollectionAssertionSource integration
  • Loading branch information
thomhurst committed Oct 17, 2025
commit d1bf9050ea2bf5a8815d935b27545442adb734ef
6 changes: 3 additions & 3 deletions TUnit.Assertions.Tests/TypeOfTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ public async Task IsTypeOf_IEnumerableToArray_Success()
{
IEnumerable<int> enumerable = new int[] { 10, 20, 30 };

var result = await Assert.That(enumerable).IsTypeOf<int[]>();
var result = await Assert.That(enumerable).IsTypeOf<int[], IEnumerable<int>>();

await Assert.That(result.Length).IsEqualTo(3);
await Assert.That(result[1]).IsEqualTo(20);
Expand Down Expand Up @@ -401,7 +401,7 @@ public async Task IsTypeOf_IEnumerableToHashSet_Success()
{
IEnumerable<string> enumerable = new HashSet<string> { "alpha", "beta", "gamma" };

var result = await Assert.That(enumerable).IsTypeOf<HashSet<string>>();
var result = await Assert.That(enumerable).IsTypeOf<HashSet<string>, IEnumerable<string>>();

await Assert.That(result.Count).IsEqualTo(3);
await Assert.That(result.Contains("beta")).IsTrue();
Expand Down Expand Up @@ -607,7 +607,7 @@ public async Task IsTypeOf_LinkedListFromIEnumerable_Success()
{
IEnumerable<int> enumerable = new LinkedList<int>(new[] { 10, 20, 30 });

var result = await Assert.That(enumerable).IsTypeOf<LinkedList<int>>();
var result = await Assert.That(enumerable).IsTypeOf<LinkedList<int>, IEnumerable<int>>();

await Assert.That(result.Count).IsEqualTo(3);
await Assert.That(result.First!.Value).IsEqualTo(10);
Expand Down
9 changes: 2 additions & 7 deletions TUnit.Assertions/Chaining/AndContinuation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,10 @@ namespace TUnit.Assertions.Core;
/// Implements IAssertionSource so all extension methods work automatically.
/// </summary>
/// <typeparam name="TValue">The type of value being asserted</typeparam>
public class AndContinuation<TValue> : IAssertionSource<TValue>
public class AndContinuation<TValue> : ContinuationBase<TValue>
{
public AssertionContext<TValue> Context { get; }

internal AndContinuation(AssertionContext<TValue> context, Assertion<TValue> previousAssertion)
: base(context, previousAssertion, ".And", CombinerType.And)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
context.ExpressionBuilder.Append(".And");
// Set pending link state for next assertion to consume
context.SetPendingLink(previousAssertion, CombinerType.And);
}
}
9 changes: 2 additions & 7 deletions TUnit.Assertions/Chaining/OrContinuation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,10 @@ namespace TUnit.Assertions.Core;
/// Implements IAssertionSource so all extension methods work automatically.
/// </summary>
/// <typeparam name="TValue">The type of value being asserted</typeparam>
public class OrContinuation<TValue> : IAssertionSource<TValue>
public class OrContinuation<TValue> : ContinuationBase<TValue>
{
public AssertionContext<TValue> Context { get; }

internal OrContinuation(AssertionContext<TValue> context, Assertion<TValue> previousAssertion)
: base(context, previousAssertion, ".Or", CombinerType.Or)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
context.ExpressionBuilder.Append(".Or");
// Set pending link state for next assertion to consume
context.SetPendingLink(previousAssertion, CombinerType.Or);
}
}
24 changes: 14 additions & 10 deletions TUnit.Assertions/Core/Assertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ public Assertion<TValue> Because(string message)
/// </summary>
public TaskAwaiter<TValue?> GetAwaiter() => AssertAsync().GetAwaiter();

/// <summary>
/// Helper method to check if we're mixing And/Or combiners and throw if so.
/// </summary>
protected void ThrowIfMixingCombiner<TCombinerToAvoid>()
where TCombinerToAvoid : Assertion<TValue>
{
if (_wrappedExecution is TCombinerToAvoid)
{
throw new MixedAndOrAssertionsException();
}
}

/// <summary>
/// Creates an And continuation for chaining additional assertions.
/// All assertions in an And chain must pass.
Expand All @@ -170,11 +182,7 @@ public AndContinuation<TValue> And
{
get
{
// Check if we're chaining And after Or (mixing combiners)
if (_wrappedExecution is Chaining.OrAssertion<TValue>)
{
throw new Exceptions.MixedAndOrAssertionsException();
}
ThrowIfMixingCombiner<Chaining.OrAssertion<TValue>>();
return new(Context, _wrappedExecution ?? this);
}
}
Expand All @@ -187,11 +195,7 @@ public OrContinuation<TValue> Or
{
get
{
// Check if we're chaining Or after And (mixing combiners)
if (_wrappedExecution is Chaining.AndAssertion<TValue>)
{
throw new Exceptions.MixedAndOrAssertionsException();
}
ThrowIfMixingCombiner<Chaining.AndAssertion<TValue>>();
return new(Context, _wrappedExecution ?? this);
}
}
Expand Down
18 changes: 4 additions & 14 deletions TUnit.Assertions/Core/CollectionContinuations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,24 @@ namespace TUnit.Assertions.Core;
/// And continuation for collection assertions that preserves collection type and item type.
/// Implements ICollectionAssertionSource to enable all collection extension methods.
/// </summary>
public class CollectionAndContinuation<TCollection, TItem> : ICollectionAssertionSource<TCollection, TItem>
public class CollectionAndContinuation<TCollection, TItem> : ContinuationBase<TCollection>, ICollectionAssertionSource<TCollection, TItem>
where TCollection : IEnumerable<TItem>
{
public AssertionContext<TCollection> Context { get; }

internal CollectionAndContinuation(AssertionContext<TCollection> context, Assertion<TCollection> previousAssertion)
: base(context, previousAssertion, ".And", CombinerType.And)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
context.ExpressionBuilder.Append(".And");
// Set pending link for the next assertion to consume
context.SetPendingLink(previousAssertion, CombinerType.And);
}
}

/// <summary>
/// Or continuation for collection assertions that preserves collection type and item type.
/// Implements ICollectionAssertionSource to enable all collection extension methods.
/// </summary>
public class CollectionOrContinuation<TCollection, TItem> : ICollectionAssertionSource<TCollection, TItem>
public class CollectionOrContinuation<TCollection, TItem> : ContinuationBase<TCollection>, ICollectionAssertionSource<TCollection, TItem>
where TCollection : IEnumerable<TItem>
{
public AssertionContext<TCollection> Context { get; }

internal CollectionOrContinuation(AssertionContext<TCollection> context, Assertion<TCollection> previousAssertion)
: base(context, previousAssertion, ".Or", CombinerType.Or)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
context.ExpressionBuilder.Append(".Or");
// Set pending link for the next assertion to consume
context.SetPendingLink(previousAssertion, CombinerType.Or);
}
}
23 changes: 23 additions & 0 deletions TUnit.Assertions/Core/ContinuationBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace TUnit.Assertions.Core;

/// <summary>
/// Base class for And/Or continuations that provides common initialization logic.
/// All continuations implement IAssertionSource to enable extension methods.
/// </summary>
/// <typeparam name="TValue">The type of value being asserted</typeparam>
public abstract class ContinuationBase<TValue> : IAssertionSource<TValue>
{
public AssertionContext<TValue> Context { get; }

internal ContinuationBase(
AssertionContext<TValue> context,
Assertion<TValue> previousAssertion,
string combinerExpression,
CombinerType combinerType)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
context.ExpressionBuilder.Append(combinerExpression);
// Set pending link state for next assertion to consume
context.SetPendingLink(previousAssertion, combinerType);
}
}
10 changes: 9 additions & 1 deletion TUnit.Assertions/Core/IAssertionSource.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
namespace TUnit.Assertions.Core;

/// <summary>
/// Non-generic base interface for all assertion sources.
/// Used for extension methods that need single type parameter (like IsTypeOf).
/// </summary>
public interface IAssertionSource
{
}

/// <summary>
/// Common interface for all assertion sources (assertions and continuations).
/// Extension methods target this interface, eliminating duplication.
/// </summary>
/// <typeparam name="TValue">The type of value being asserted</typeparam>
public interface IAssertionSource<TValue>
public interface IAssertionSource<TValue> : IAssertionSource
{
/// <summary>
/// The assertion context shared by all assertions in this chain.
Expand Down
13 changes: 7 additions & 6 deletions TUnit.Assertions/Extensions/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ public static LessThanAssertion<TValue> IsNegative<TValue>(
/// <summary>
/// Asserts that the value is of the specified type and returns an assertion on the casted value.
/// Example: await Assert.That(obj).IsTypeOf<StringBuilder>();
/// This generic overload works with any source type.
/// Single type parameter version - infers source type.
/// </summary>
public static TypeOfAssertion<TValue, TExpected> IsTypeOf<TExpected, TValue>(
this IAssertionSource<TValue> source)
Expand All @@ -335,14 +335,15 @@ public static TypeOfAssertion<TValue, TExpected> IsTypeOf<TExpected, TValue>(
}

/// <summary>
/// Asserts that the value is of the specified type and returns an assertion on the casted value (specialized for object).
/// Example: await Assert.That(obj).IsTypeOf<StringBuilder>();
/// Asserts that the IEnumerable is of the specified type and returns an assertion on the casted value.
/// Specialized overload for IEnumerable assertions with better type inference.
/// Example: await Assert.That(enumerable).IsTypeOf<int[]>() where enumerable is IEnumerable<int>
/// </summary>
public static TypeOfAssertion<object, TExpected> IsTypeOf<TExpected>(
this IAssertionSource<object> source)
public static TypeOfAssertion<IEnumerable<TItem>, TExpected> IsTypeOf<TExpected, TItem>(
this IAssertionSource<IEnumerable<TItem>> source)
{
source.Context.ExpressionBuilder.Append($".IsTypeOf<{typeof(TExpected).Name}>()");
return new TypeOfAssertion<object, TExpected>(source.Context);
return new TypeOfAssertion<IEnumerable<TItem>, TExpected>(source.Context);
}

/// <summary>
Expand Down
Loading
Loading