Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
69 changes: 69 additions & 0 deletions TUnit.Assertions.Tests/IsAssignableToTypedReturnTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Text;

namespace TUnit.Assertions.Tests;

/// <summary>
/// Tests for GitHub issue #6184: <c>IsAssignableTo&lt;T&gt;()</c> should return the value
/// cast to <c>T</c> (parity with <c>IsTypeOf&lt;T&gt;()</c>), so the caller doesn't need a
/// second manual cast.
/// </summary>
public class IsAssignableToTypedReturnTests
{
private interface IElement { }
private class Element : IElement { }
private class DerivedElement : Element { }

[Test]
public async Task IsAssignableTo_ReturnsValueCastToInterface()
{
// The exact scenario from the issue: an object/base-typed value that we want back
// as the interface it implements.
object obj = new List<string> { "a", "b", "c" };

IEnumerable<string> result = await Assert.That(obj).IsAssignableTo<IEnumerable<string>>();

await Assert.That(result.Count()).IsEqualTo(3);
await Assert.That(result.First()).IsEqualTo("a");
}

[Test]
public async Task IsAssignableTo_ReturnsValueCastToBaseType()
{
DerivedElement derived = new DerivedElement();

Element result = (await Assert.That(derived).IsAssignableTo<Element>())!;

await Assert.That(result).IsSameReferenceAs(derived);
}

[Test]
public async Task IsAssignableTo_ExactType_ReturnsValue()
{
var sb = new StringBuilder("Test");
object obj = sb;

StringBuilder result = (await Assert.That(obj).IsAssignableTo<StringBuilder>())!;

await Assert.That(result.ToString()).IsEqualTo("Test");
}

[Test]
public async Task IsAssignableTo_NotAssignable_StillFails()
{
await Assert.ThrowsAsync<TUnit.Assertions.Exceptions.AssertionException>(async () =>
{
Element element = new Element();
await Assert.That(element).IsAssignableTo<DerivedElement>();
});
}

[Test]
public async Task IsAssignableTo_Null_StillFails()
{
await Assert.ThrowsAsync<TUnit.Assertions.Exceptions.AssertionException>(async () =>
{
object? obj = null;
await Assert.That(obj).IsAssignableTo<IElement>();
});
}
}
29 changes: 17 additions & 12 deletions TUnit.Assertions/Conditions/TypeOfAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,26 +91,31 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> m
}

/// <summary>
/// Asserts that a value's type is assignable to a specific type (is the type or a subtype).
/// Asserts that a value's type is assignable to a specific type (is the type or a subtype),
/// and transforms the assertion chain to that type so the awaited result is the typed value.
/// Works with both direct value assertions and exception assertions (via .And after Throws).
/// </summary>
public class IsAssignableToAssertion<TTarget, TValue> : Assertion<TValue>
public class IsAssignableToAssertion<TTarget, TValue> : Assertion<TTarget>
{
private readonly Type _targetType;
// The original (pre-map) context. Both this and the mapped base context share the same
// cached underlying evaluation, so the source is still evaluated only once. We read from
// it during the check to preserve the original value/exception type for validation and
// error messages (the mapped value is null when the cast doesn't apply).
private readonly AssertionContext<TValue> _sourceContext;
private readonly Type _targetType = typeof(TTarget);

public IsAssignableToAssertion(
AssertionContext<TValue> context)
: base(context)
: base(context.Map<TTarget>(value => value is TTarget casted ? casted : default))
{
_targetType = typeof(TTarget);
_sourceContext = context;
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> metadata)
protected override async Task<AssertionResult> CheckAsync(EvaluationMetadata<TTarget> metadata)
{
var value = metadata.Value;
var exception = metadata.Exception;
var (value, exception) = await _sourceContext.GetAsync();

object? objectToCheck = null;
object? objectToCheck;

// If we have an exception (from Throws/ThrowsExactly), check that
if (exception != null)
Expand All @@ -124,17 +129,17 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> m
}
else
{
return Task.FromResult(AssertionResult.Failed("value was null"));
return AssertionResult.Failed("value was null");
}

var actualType = objectToCheck.GetType();

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

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

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