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
12 changes: 12 additions & 0 deletions TUnit.Assertions/Assertions/PropertyAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ public IsNotAssignableToAssertion<TTarget, TObject> IsNotAssignableTo<TTarget>()
return new IsNotAssignableToAssertion<TTarget, TObject>(Context);
}

public IsAssignableFromAssertion<TTarget, TObject> IsAssignableFrom<TTarget>()
{
Context.ExpressionBuilder.Append($".IsAssignableFrom<{typeof(TTarget).Name}>()");
return new IsAssignableFromAssertion<TTarget, TObject>(Context);
}

public IsNotAssignableFromAssertion<TTarget, TObject> IsNotAssignableFrom<TTarget>()
{
Context.ExpressionBuilder.Append($".IsNotAssignableFrom<{typeof(TTarget).Name}>()");
return new IsNotAssignableFromAssertion<TTarget, TObject>(Context);
}

/// <summary>
/// Asserts that the parent object is NOT of the specified type.
/// Example: await Assert.That(obj).HasProperty(x => x.Name).IsEqualTo("test").IsNotTypeOf<BaseClass>();
Expand Down
12 changes: 12 additions & 0 deletions TUnit.Assertions/Assertions/Strings/ParseAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,18 @@ public IsNotAssignableToAssertion<TTarget, T> IsNotAssignableTo<TTarget>()
return new IsNotAssignableToAssertion<TTarget, T>(Context);
}

public IsAssignableFromAssertion<TTarget, T> IsAssignableFrom<TTarget>()
{
Context.ExpressionBuilder.Append($".IsAssignableFrom<{typeof(TTarget).Name}>()");
return new IsAssignableFromAssertion<TTarget, T>(Context);
}

public IsNotAssignableFromAssertion<TTarget, T> IsNotAssignableFrom<TTarget>()
{
Context.ExpressionBuilder.Append($".IsNotAssignableFrom<{typeof(TTarget).Name}>()");
return new IsNotAssignableFromAssertion<TTarget, T>(Context);
}

/// <summary>
/// Asserts that the parsed value is NOT of the specified type.
/// Example: await Assert.That("123").WhenParsedInto<object>().IsNotTypeOf<string>();
Expand Down
28 changes: 28 additions & 0 deletions TUnit.Assertions/Conditions/ListAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,20 @@ public IsNotAssignableToAssertion<TExpected, TItem> IsNotAssignableTo<TExpected>
Context.ExpressionBuilder.Append($".IsNotAssignableTo<{typeof(TExpected).Name}>()");
return new IsNotAssignableToAssertion<TExpected, TItem>(Context);
}

/// <inheritdoc />
public IsAssignableFromAssertion<TExpected, TItem> IsAssignableFrom<TExpected>()
{
Context.ExpressionBuilder.Append($".IsAssignableFrom<{typeof(TExpected).Name}>()");
return new IsAssignableFromAssertion<TExpected, TItem>(Context);
}

/// <inheritdoc />
public IsNotAssignableFromAssertion<TExpected, TItem> IsNotAssignableFrom<TExpected>()
{
Context.ExpressionBuilder.Append($".IsNotAssignableFrom<{typeof(TExpected).Name}>()");
return new IsNotAssignableFromAssertion<TExpected, TItem>(Context);
}
}

/// <summary>
Expand Down Expand Up @@ -259,6 +273,20 @@ public IsNotAssignableToAssertion<TExpected, TItem> IsNotAssignableTo<TExpected>
Context.ExpressionBuilder.Append($".IsNotAssignableTo<{typeof(TExpected).Name}>()");
return new IsNotAssignableToAssertion<TExpected, TItem>(Context);
}

/// <inheritdoc />
public IsAssignableFromAssertion<TExpected, TItem> IsAssignableFrom<TExpected>()
{
Context.ExpressionBuilder.Append($".IsAssignableFrom<{typeof(TExpected).Name}>()");
return new IsAssignableFromAssertion<TExpected, TItem>(Context);
}

/// <inheritdoc />
public IsNotAssignableFromAssertion<TExpected, TItem> IsNotAssignableFrom<TExpected>()
{
Context.ExpressionBuilder.Append($".IsNotAssignableFrom<{typeof(TExpected).Name}>()");
return new IsNotAssignableFromAssertion<TExpected, TItem>(Context);
}
}

/// <summary>
Expand Down
12 changes: 12 additions & 0 deletions TUnit.Assertions/Conditions/MemberAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,18 @@ public IsNotAssignableToAssertion<TTarget, T> IsNotAssignableTo<TTarget>()
return new IsNotAssignableToAssertion<TTarget, T>(Context);
}

public IsAssignableFromAssertion<TTarget, T> IsAssignableFrom<TTarget>()
{
Context.ExpressionBuilder.Append($".IsAssignableFrom<{typeof(TTarget).Name}>()");
return new IsAssignableFromAssertion<TTarget, T>(Context);
}

public IsNotAssignableFromAssertion<TTarget, T> IsNotAssignableFrom<TTarget>()
{
Context.ExpressionBuilder.Append($".IsNotAssignableFrom<{typeof(TTarget).Name}>()");
return new IsNotAssignableFromAssertion<TTarget, T>(Context);
}

/// <summary>
/// Asserts that the value is NOT of the specified type.
/// Example: await Assert.That(obj).Member(x => x.Property).Satisfies(val => val.IsNotTypeOf<int>());
Expand Down
28 changes: 28 additions & 0 deletions TUnit.Assertions/Conditions/ReadOnlyListAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,20 @@ public IsNotAssignableToAssertion<TExpected, TItem> IsNotAssignableTo<TExpected>
Context.ExpressionBuilder.Append($".IsNotAssignableTo<{typeof(TExpected).Name}>()");
return new IsNotAssignableToAssertion<TExpected, TItem>(Context);
}

/// <inheritdoc />
public IsAssignableFromAssertion<TExpected, TItem> IsAssignableFrom<TExpected>()
{
Context.ExpressionBuilder.Append($".IsAssignableFrom<{typeof(TExpected).Name}>()");
return new IsAssignableFromAssertion<TExpected, TItem>(Context);
}

/// <inheritdoc />
public IsNotAssignableFromAssertion<TExpected, TItem> IsNotAssignableFrom<TExpected>()
{
Context.ExpressionBuilder.Append($".IsNotAssignableFrom<{typeof(TExpected).Name}>()");
return new IsNotAssignableFromAssertion<TExpected, TItem>(Context);
}
}

/// <summary>
Expand Down Expand Up @@ -261,6 +275,20 @@ public IsNotAssignableToAssertion<TExpected, TItem> IsNotAssignableTo<TExpected>
Context.ExpressionBuilder.Append($".IsNotAssignableTo<{typeof(TExpected).Name}>()");
return new IsNotAssignableToAssertion<TExpected, TItem>(Context);
}

/// <inheritdoc />
public IsAssignableFromAssertion<TExpected, TItem> IsAssignableFrom<TExpected>()
{
Context.ExpressionBuilder.Append($".IsAssignableFrom<{typeof(TExpected).Name}>()");
return new IsAssignableFromAssertion<TExpected, TItem>(Context);
}

/// <inheritdoc />
public IsNotAssignableFromAssertion<TExpected, TItem> IsNotAssignableFrom<TExpected>()
{
Context.ExpressionBuilder.Append($".IsNotAssignableFrom<{typeof(TExpected).Name}>()");
return new IsNotAssignableFromAssertion<TExpected, TItem>(Context);
}
}

/// <summary>
Expand Down
100 changes: 100 additions & 0 deletions TUnit.Assertions/Conditions/TypeOfAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,106 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> m
protected override string GetExpectation() => $"to not be assignable to {_targetType.Name}";
}

/// <summary>
/// Asserts that a value's type is assignable from a specific type (the value's type is a base type or same as TTarget).
/// Works with both direct value assertions and exception assertions (via .And after Throws).
/// </summary>
public class IsAssignableFromAssertion<TTarget, TValue> : Assertion<TValue>
{
private readonly Type _targetType;

public IsAssignableFromAssertion(
AssertionContext<TValue> context)
: base(context)
{
_targetType = typeof(TTarget);
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> metadata)
{
var value = metadata.Value;
var exception = metadata.Exception;

object? objectToCheck = null;

// If we have an exception (from Throws/ThrowsExactly), check that
if (exception != null)
{
objectToCheck = exception;
}
// Otherwise check the value
else if (value != null)
{
objectToCheck = value;
}
else
{
return Task.FromResult(AssertionResult.Failed("value was null"));
}

var actualType = objectToCheck.GetType();

if (actualType.IsAssignableFrom(_targetType))
{
return AssertionResult._passedTask;
}

return Task.FromResult(AssertionResult.Failed($"type {actualType.Name} is not assignable from {_targetType.Name}"));
}

protected override string GetExpectation() => $"to be assignable from {_targetType.Name}";
}

/// <summary>
/// Asserts that a value's type is NOT assignable from a specific type.
/// Works with both direct value assertions and exception assertions (via .And after Throws).
/// </summary>
public class IsNotAssignableFromAssertion<TTarget, TValue> : Assertion<TValue>
{
private readonly Type _targetType;

public IsNotAssignableFromAssertion(
AssertionContext<TValue> context)
: base(context)
{
_targetType = typeof(TTarget);
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> metadata)
{
var value = metadata.Value;
var exception = metadata.Exception;

object? objectToCheck = null;

// If we have an exception (from Throws/ThrowsExactly), check that
if (exception != null)
{
objectToCheck = exception;
}
// Otherwise check the value
else if (value != null)
{
objectToCheck = value;
}
else
{
return Task.FromResult(AssertionResult.Failed("value was null"));
}

var actualType = objectToCheck.GetType();

if (!actualType.IsAssignableFrom(_targetType))
{
return AssertionResult._passedTask;
}

return Task.FromResult(AssertionResult.Failed($"type {actualType.Name} is assignable from {_targetType.Name}"));
}

protected override string GetExpectation() => $"to not be assignable from {_targetType.Name}";
}

/// <summary>
/// Asserts that a value is exactly of the specified type (using runtime Type parameter).
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions TUnit.Assertions/Conditions/Wrappers/CountWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ IsNotAssignableToAssertion<TTarget, TCollection> IAssertionSource<TCollection>.I
"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>
Expand Down
20 changes: 20 additions & 0 deletions TUnit.Assertions/Conditions/Wrappers/LengthWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ IsNotAssignableToAssertion<TTarget, string> IAssertionSource<string>.IsNotAssign
"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>
Expand Down
10 changes: 10 additions & 0 deletions TUnit.Assertions/Core/IAssertionSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,14 @@ public interface IAssertionSource<TValue> : IAssertionSource
/// Asserts that the value's type is NOT assignable to the specified type.
/// </summary>
IsNotAssignableToAssertion<TExpected, TValue> IsNotAssignableTo<TExpected>();

/// <summary>
/// Asserts that the value's type is assignable from the specified type.
/// </summary>
IsAssignableFromAssertion<TExpected, TValue> IsAssignableFrom<TExpected>();

/// <summary>
/// Asserts that the value's type is NOT assignable from the specified type.
/// </summary>
IsNotAssignableFromAssertion<TExpected, TValue> IsNotAssignableFrom<TExpected>();
}
32 changes: 32 additions & 0 deletions TUnit.Assertions/Sources/AsyncDelegateAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@ TypeOfAssertion<Task, TExpected> IAssertionSource<Task>.IsTypeOf<TExpected>()
return new IsNotAssignableToAssertion<TTarget, object?>(Context);
}

public IsAssignableFromAssertion<TTarget, object?> IsAssignableFrom<TTarget>()
{
Context.ExpressionBuilder.Append($".IsAssignableFrom<{typeof(TTarget).Name}>()");
return new IsAssignableFromAssertion<TTarget, object?>(Context);
}

public IsNotAssignableFromAssertion<TTarget, object?> IsNotAssignableFrom<TTarget>()
{
Context.ExpressionBuilder.Append($".IsNotAssignableFrom<{typeof(TTarget).Name}>()");
return new IsNotAssignableFromAssertion<TTarget, object?>(Context);
}

/// <summary>
/// Explicit interface implementation for Task assignability checking.
/// Asserts that the task itself is assignable to the specified type.
Expand All @@ -173,6 +185,26 @@ IsNotAssignableToAssertion<TTarget, Task> IAssertionSource<Task>.IsNotAssignable
return new IsNotAssignableToAssertion<TTarget, Task>(TaskContext);
}

/// <summary>
/// Explicit interface implementation for Task assignability checking.
/// Asserts that the task itself is assignable from the specified type.
/// </summary>
IsAssignableFromAssertion<TTarget, Task> IAssertionSource<Task>.IsAssignableFrom<TTarget>()
{
TaskContext.ExpressionBuilder.Append($".IsAssignableFrom<{typeof(TTarget).Name}>()");
return new IsAssignableFromAssertion<TTarget, Task>(TaskContext);
}

/// <summary>
/// Explicit interface implementation for Task assignability checking.
/// Asserts that the task itself is not assignable from the specified type.
/// </summary>
IsNotAssignableFromAssertion<TTarget, Task> IAssertionSource<Task>.IsNotAssignableFrom<TTarget>()
{
TaskContext.ExpressionBuilder.Append($".IsNotAssignableFrom<{typeof(TTarget).Name}>()");
return new IsNotAssignableFromAssertion<TTarget, Task>(TaskContext);
}

/// <summary>
/// Asserts that the value is NOT of the specified type.
/// Example: await Assert.That(async () => await SomeMethodAsync()).IsNotTypeOf<int>();
Expand Down
14 changes: 14 additions & 0 deletions TUnit.Assertions/Sources/AsyncEnumerableAssertionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,20 @@ public IsNotAssignableToAssertion<TExpected, IAsyncEnumerable<TItem>> IsNotAssig
return new IsNotAssignableToAssertion<TExpected, IAsyncEnumerable<TItem>>(Context);
}

/// <inheritdoc />
public IsAssignableFromAssertion<TExpected, IAsyncEnumerable<TItem>> IsAssignableFrom<TExpected>()
{
Context.ExpressionBuilder.Append($".IsAssignableFrom<{typeof(TExpected).Name}>()");
return new IsAssignableFromAssertion<TExpected, IAsyncEnumerable<TItem>>(Context);
}

/// <inheritdoc />
public IsNotAssignableFromAssertion<TExpected, IAsyncEnumerable<TItem>> IsNotAssignableFrom<TExpected>()
{
Context.ExpressionBuilder.Append($".IsNotAssignableFrom<{typeof(TExpected).Name}>()");
return new IsNotAssignableFromAssertion<TExpected, IAsyncEnumerable<TItem>>(Context);
}

// CheckAsync is not overridden here - it's abstract in Assertion<T>
// Each concrete assertion class will implement it
}
Loading
Loading