Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 src/TUnit.Assertions/Conditions/TypeAssertionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using TUnit.Assertions.Attributes;
using TUnit.Assertions.Core;

namespace TUnit.Assertions.Conditions;

Expand Down Expand Up @@ -83,4 +84,15 @@ namespace TUnit.Assertions.Conditions;
[AssertionFrom<Type>(nameof(Type.IsCOMObject), CustomName = "IsNotCOMObject", NegateLogic = true, ExpectationMessage = "be a COM object")]
public static partial class TypeAssertionExtensions
{
[GenerateAssertion(ExpectationMessage = "be assignable to {expectedType}", InlineMethodBody = true)]
public static AssertionResult IsAssignableTo(this Type value, Type expectedType)
Comment thread
thomhurst marked this conversation as resolved.
Comment thread
thomhurst marked this conversation as resolved.
=> expectedType.IsAssignableFrom(value)
? AssertionResult.Passed
: AssertionResult.Failed($"type {value.Name} is not assignable to {expectedType.Name}");

[GenerateAssertion(ExpectationMessage = "be assignable from {sourceType}", InlineMethodBody = true)]
public static AssertionResult IsAssignableFrom(this Type value, Type sourceType)
=> value.IsAssignableFrom(sourceType)
? AssertionResult.Passed
: AssertionResult.Failed($"type {value.Name} is not assignable from {sourceType.Name}");
}
42 changes: 38 additions & 4 deletions src/TUnit.Assertions/Conditions/TypeOfAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ protected override async Task<AssertionResult> CheckAsync(EvaluationMetadata<TTa
return AssertionResult.Failed("value was null");
}

var actualType = objectToCheck.GetType();
var actualType = objectToCheck is Type typeToCheck ? typeToCheck : objectToCheck.GetType();
Comment thread
thomhurst marked this conversation as resolved.
Outdated
Comment thread
thomhurst marked this conversation as resolved.
Outdated

if (_targetType.IsAssignableFrom(actualType))
Comment thread
thomhurst marked this conversation as resolved.
Outdated
{
Expand All @@ -147,6 +147,40 @@ protected override async Task<AssertionResult> CheckAsync(EvaluationMetadata<TTa
protected override string GetExpectation() => $"to be assignable to {_targetType.Name}";
}

/// <summary>
/// Asserts that a represented <see cref="Type"/> is assignable to a target type while
/// retaining the represented type as the assertion value.
/// </summary>
public sealed class TypeIsAssignableToAssertion<TTarget> : Assertion<Type>
{
private readonly Type _targetType = typeof(TTarget);

public TypeIsAssignableToAssertion(AssertionContext<Type> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<Type> metadata)
{
if (metadata.Exception is { } exception)
{
return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().Name}", exception));
}

if (metadata.Value is not { } representedType)
{
return Task.FromResult(AssertionResult.Failed("value was null"));
}

return _targetType.IsAssignableFrom(representedType)
? AssertionResult._passedTask
: Task.FromResult(AssertionResult.Failed(
$"type {representedType.Name} is not assignable to {_targetType.Name}"));
}

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

/// <summary>
/// Asserts that a value's type is NOT assignable to a specific type.
/// Works with both direct value assertions and exception assertions (via .And after Throws).
Expand Down Expand Up @@ -184,7 +218,7 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> m
return Task.FromResult(AssertionResult.Failed("value was null"));
}
Comment thread
thomhurst marked this conversation as resolved.

var actualType = objectToCheck.GetType();
var actualType = objectToCheck is Type typeToCheck ? typeToCheck : objectToCheck.GetType();

if (!_targetType.IsAssignableFrom(actualType))
{
Expand Down Expand Up @@ -233,7 +267,7 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> m
return Task.FromResult(AssertionResult.Failed("value was null"));
}

var actualType = objectToCheck.GetType();
var actualType = objectToCheck is Type typeToCheck ? typeToCheck : objectToCheck.GetType();

if (actualType.IsAssignableFrom(_sourceType))
{
Expand Down Expand Up @@ -281,7 +315,7 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> m
return Task.FromResult(AssertionResult.Failed("value was null"));
}

var actualType = objectToCheck.GetType();
var actualType = objectToCheck is Type typeToCheck ? typeToCheck : objectToCheck.GetType();

if (!actualType.IsAssignableFrom(_sourceType))
{
Expand Down
10 changes: 10 additions & 0 deletions src/TUnit.Assertions/Extensions/Assert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,16 @@ public static HashSetAssertion<TItem> That<TItem>(
return new CollectionAssertion<object?>(value.Cast<object?>(), expression);
}

/// <summary>
/// Creates an assertion for a represented <see cref="Type"/>.
/// </summary>
public static TypeValueAssertion That(
Type? value,
[CallerArgumentExpression(nameof(value))] string? expression = null)
Comment thread
thomhurst marked this conversation as resolved.
{
return new TypeValueAssertion(value, expression);
}

/// <summary>
/// Creates an assertion for an immediate value.
/// Example: await Assert.That(42).IsEqualTo(42);
Expand Down
25 changes: 25 additions & 0 deletions src/TUnit.Assertions/Sources/TypeValueAssertion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using TUnit.Assertions.Conditions;
using TUnit.Assertions.Core;

namespace TUnit.Assertions.Sources;

/// <summary>
/// Source assertion for represented types.
/// </summary>
public sealed class TypeValueAssertion : ValueAssertion<Type>
{
public TypeValueAssertion(Type? value, string? expression)
: base(value, expression)
{
}

/// <summary>
/// Asserts that the represented type is assignable to <typeparamref name="TTarget"/>.
/// The assertion retains the represented <see cref="Type"/> for awaiting and chaining.
/// </summary>
public new TypeIsAssignableToAssertion<TTarget> IsAssignableTo<TTarget>()
{
Context.ExpressionBuilder.Append($".IsAssignableTo<{typeof(TTarget).Name}>()");
return new TypeIsAssignableToAssertion<TTarget>(Context);
}
}
37 changes: 37 additions & 0 deletions tests/TUnit.Assertions.Tests/TypeAssertionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ namespace TUnit.Assertions.Tests;

public class TypeAssertionTests
{
private class Animal { }
private class Dog : Animal { }

// Test types for various scenarios
private class TestClass { }
private interface ITestInterface { }
Expand Down Expand Up @@ -459,6 +462,40 @@ public async Task Test_Type_IsNotCOMObject_TestClass()
await Assert.That(type).IsNotCOMObject();
}

[Test]
public async Task Test_Type_IsAssignableTo_Generic_UsesRepresentedType()
{
Type? result = await Assert.That(typeof(Dog)).IsAssignableTo<Animal>();

await Assert.That(result).IsSameReferenceAs(typeof(Dog));
}

[Test]
public async Task Test_Type_IsAssignableTo_Generic_RetainsTypeForChaining()
{
await Assert.That(typeof(Dog))
.IsAssignableTo<Animal>()
.And.IsClass();
}

[Test]
public async Task Test_Type_IsAssignableFrom_Generic_UsesRepresentedType()
{
await Assert.That(typeof(Animal)).IsAssignableFrom<Dog>();
}

[Test]
public async Task Test_Type_IsAssignableTo_RuntimeTypeOverload_Passes()
{
await Assert.That(typeof(Dog)).IsAssignableTo(typeof(Animal));
}

[Test]
public async Task Test_Type_IsAssignableFrom_RuntimeTypeOverload_Passes()
{
await Assert.That(typeof(Animal)).IsAssignableFrom(typeof(Dog));
}

#if NET5_0_OR_GREATER
// IsByRefLike / IsNotByRefLike (NET5+)
[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ namespace
public static .<object?> That(.IEnumerable value, [.("value")] string? expression = null) { }
public static . That(<.> action, [.("action")] string? expression = null) { }
public static . That(. task, [.("task")] string? expression = null) { }
public static . That(? value, [.("value")] string? expression = null) { }
[.(2)]
public static .<string> That(.StringValue value, [.("value")] string? expression = null) { }
[.(3)]
Expand Down Expand Up @@ -2287,7 +2288,19 @@ namespace .Conditions
[.<>("IsValueType", ExpectationMessage="be a value type")]
[.<>("IsVisible", CustomName="IsNotVisible", ExpectationMessage="be visible", NegateLogic=true)]
[.<>("IsVisible", ExpectationMessage="be visible")]
public static class TypeAssertionExtensions { }
public static class TypeAssertionExtensions
{
[.(ExpectationMessage="be assignable from {sourceType}", InlineMethodBody=true)]
public static . IsAssignableFrom(this value, sourceType) { }
[.(ExpectationMessage="be assignable to {expectedType}", InlineMethodBody=true)]
public static . IsAssignableTo(this value, expectedType) { }
}
public sealed class TypeIsAssignableToAssertion<TTarget> : .<>
{
public TypeIsAssignableToAssertion(.<> context) { }
protected override .<.> CheckAsync(.<> metadata) { }
protected override string GetExpectation() { }
}
public class TypeOfAssertion<TFrom, TTo> : .<TTo>
{
public TypeOfAssertion(.<TFrom> parentContext) { }
Expand Down Expand Up @@ -6692,6 +6705,12 @@ namespace .Extensions
public static . DoesNotContainGenericParameters(this .<> source) { }
public static . IsAbstract(this .<> source) { }
public static . IsArray(this .<> source) { }
[.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")]
public static ._IsAssignableFrom_Type_Assertion IsAssignableFrom<TActual>(this .<TActual> source, sourceType, [.("sourceType")] string? sourceTypeExpression = null)
where TActual : { }
[.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")]
public static ._IsAssignableTo_Type_Assertion IsAssignableTo<TActual>(this .<TActual> source, expectedType, [.("expectedType")] string? expectedTypeExpression = null)
where TActual : { }
public static . IsByRef(this .<> source) { }
public static . IsByRefLike(this .<> source) { }
public static . IsCOMObject(this .<> source) { }
Expand Down Expand Up @@ -6873,6 +6892,18 @@ namespace .Extensions
protected override .<.> CheckAsync(.<> metadata) { }
protected override string GetExpectation() { }
}
public sealed class Type_IsAssignableFrom_Type_Assertion : .<>
{
public Type_IsAssignableFrom_Type_Assertion(.<> context, sourceType) { }
protected override .<.> CheckAsync(.<> metadata) { }
protected override string GetExpectation() { }
}
public sealed class Type_IsAssignableTo_Type_Assertion : .<>
{
public Type_IsAssignableTo_Type_Assertion(.<> context, expectedType) { }
protected override .<.> CheckAsync(.<> metadata) { }
protected override string GetExpectation() { }
}
public sealed class UInt16_IsEven_Assertion : .<ushort>
{
public UInt16_IsEven_Assertion(.<ushort> context) { }
Expand Down Expand Up @@ -7526,6 +7557,11 @@ namespace .Sources
public .<TException> ThrowsExactly<TException>()
where TException : { }
}
public sealed class TypeValueAssertion : .<>
{
public TypeValueAssertion(? value, string? expression) { }
public new .<TTarget> IsAssignableTo<TTarget>() { }
}
public class ValueAssertion<TValue> : ., .<TValue>
{
protected ValueAssertion(.<TValue> context) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ namespace
public static .<object?> That(.IEnumerable value, [.("value")] string? expression = null) { }
public static . That(<.> action, [.("action")] string? expression = null) { }
public static . That(. task, [.("task")] string? expression = null) { }
public static . That(? value, [.("value")] string? expression = null) { }
public static .<string> That(.StringValue value, [.("value")] string? expression = null) { }
public static .<TItem> That<TItem>(.<TItem>? value, [.("value")] string? expression = null) { }
public static .<TItem> That<TItem>(.<TItem> value, [.("value")] string? expression = null) { }
Expand Down Expand Up @@ -2270,7 +2271,19 @@ namespace .Conditions
[.<>("IsValueType", ExpectationMessage="be a value type")]
[.<>("IsVisible", CustomName="IsNotVisible", ExpectationMessage="be visible", NegateLogic=true)]
[.<>("IsVisible", ExpectationMessage="be visible")]
public static class TypeAssertionExtensions { }
public static class TypeAssertionExtensions
{
[.(ExpectationMessage="be assignable from {sourceType}", InlineMethodBody=true)]
public static . IsAssignableFrom(this value, sourceType) { }
[.(ExpectationMessage="be assignable to {expectedType}", InlineMethodBody=true)]
public static . IsAssignableTo(this value, expectedType) { }
}
public sealed class TypeIsAssignableToAssertion<TTarget> : .<>
{
public TypeIsAssignableToAssertion(.<> context) { }
protected override .<.> CheckAsync(.<> metadata) { }
protected override string GetExpectation() { }
}
public class TypeOfAssertion<TFrom, TTo> : .<TTo>
{
public TypeOfAssertion(.<TFrom> parentContext) { }
Expand Down Expand Up @@ -6599,6 +6612,12 @@ namespace .Extensions
public static . DoesNotContainGenericParameters(this .<> source) { }
public static . IsAbstract(this .<> source) { }
public static . IsArray(this .<> source) { }
[.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")]
public static ._IsAssignableFrom_Type_Assertion IsAssignableFrom<TActual>(this .<TActual> source, sourceType, [.("sourceType")] string? sourceTypeExpression = null)
where TActual : { }
[.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")]
public static ._IsAssignableTo_Type_Assertion IsAssignableTo<TActual>(this .<TActual> source, expectedType, [.("expectedType")] string? expectedTypeExpression = null)
where TActual : { }
public static . IsByRef(this .<> source) { }
public static . IsByRefLike(this .<> source) { }
public static . IsCOMObject(this .<> source) { }
Expand Down Expand Up @@ -6780,6 +6799,18 @@ namespace .Extensions
protected override .<.> CheckAsync(.<> metadata) { }
protected override string GetExpectation() { }
}
public sealed class Type_IsAssignableFrom_Type_Assertion : .<>
{
public Type_IsAssignableFrom_Type_Assertion(.<> context, sourceType) { }
protected override .<.> CheckAsync(.<> metadata) { }
protected override string GetExpectation() { }
}
public sealed class Type_IsAssignableTo_Type_Assertion : .<>
{
public Type_IsAssignableTo_Type_Assertion(.<> context, expectedType) { }
protected override .<.> CheckAsync(.<> metadata) { }
protected override string GetExpectation() { }
}
public sealed class UInt16_IsEven_Assertion : .<ushort>
{
public UInt16_IsEven_Assertion(.<ushort> context) { }
Expand Down Expand Up @@ -7432,6 +7463,11 @@ namespace .Sources
public .<TException> ThrowsExactly<TException>()
where TException : { }
}
public sealed class TypeValueAssertion : .<>
{
public TypeValueAssertion(? value, string? expression) { }
public new .<TTarget> IsAssignableTo<TTarget>() { }
}
public class ValueAssertion<TValue> : ., .<TValue>
{
protected ValueAssertion(.<TValue> context) { }
Expand Down
Loading
Loading