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
18 changes: 18 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,21 @@ 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 switch
{
null => AssertionResult.Failed("expected type was null"),
_ when 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)
=> sourceType switch
{
null => AssertionResult.Failed("source type was null"),
_ when value.IsAssignableFrom(sourceType) => AssertionResult.Passed,
_ => AssertionResult.Failed($"type {value.Name} is not assignable from {sourceType.Name}"),
};
}
73 changes: 70 additions & 3 deletions src/TUnit.Assertions/Conditions/TypeOfAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,61 @@ 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).
/// </summary>
public class IsNotAssignableToAssertion<TTarget, TValue> : Assertion<TValue>
{
private readonly bool _useRepresentedType;
private readonly Type _targetType;

public IsNotAssignableToAssertion(
AssertionContext<TValue> context)
: this(context, useRepresentedType: false)
{
}

internal IsNotAssignableToAssertion(
AssertionContext<TValue> context,
bool useRepresentedType)
: base(context)
{
_useRepresentedType = useRepresentedType;
_targetType = typeof(TTarget);
}

Expand All @@ -184,7 +227,9 @@ 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 = _useRepresentedType && objectToCheck is Type representedType
? representedType
: objectToCheck.GetType();

if (!_targetType.IsAssignableFrom(actualType))
{
Expand All @@ -204,12 +249,21 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> m
/// </summary>
public class IsAssignableFromAssertion<TSource, TValue> : Assertion<TValue>
{
private readonly bool _useRepresentedType;
private readonly Type _sourceType;

public IsAssignableFromAssertion(
AssertionContext<TValue> context)
: this(context, useRepresentedType: false)
{
}

internal IsAssignableFromAssertion(
AssertionContext<TValue> context,
bool useRepresentedType)
: base(context)
{
_useRepresentedType = useRepresentedType;
_sourceType = typeof(TSource);
}

Expand All @@ -233,7 +287,9 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> m
return Task.FromResult(AssertionResult.Failed("value was null"));
}

var actualType = objectToCheck.GetType();
var actualType = _useRepresentedType && objectToCheck is Type representedType
? representedType
: objectToCheck.GetType();

if (actualType.IsAssignableFrom(_sourceType))
{
Expand All @@ -252,12 +308,21 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> m
/// </summary>
public class IsNotAssignableFromAssertion<TSource, TValue> : Assertion<TValue>
{
private readonly bool _useRepresentedType;
private readonly Type _sourceType;

public IsNotAssignableFromAssertion(
AssertionContext<TValue> context)
: this(context, useRepresentedType: false)
{
}

internal IsNotAssignableFromAssertion(
AssertionContext<TValue> context,
bool useRepresentedType)
: base(context)
{
_useRepresentedType = useRepresentedType;
_sourceType = typeof(TSource);
}

Expand All @@ -281,7 +346,9 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> m
return Task.FromResult(AssertionResult.Failed("value was null"));
}

var actualType = objectToCheck.GetType();
var actualType = _useRepresentedType && objectToCheck is Type representedType
? representedType
: objectToCheck.GetType();

if (!actualType.IsAssignableFrom(_sourceType))
{
Expand Down
11 changes: 11 additions & 0 deletions src/TUnit.Assertions/Extensions/Assert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,17 @@ 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>
[OverloadResolutionPriority(1)]
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
68 changes: 68 additions & 0 deletions src/TUnit.Assertions/Sources/TypeValueAssertion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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>
/// <remarks>
/// Represented-type semantics apply only when this is the first assertion after <c>Assert.That(type)</c>.
/// After <c>.And</c> or <c>.Or</c>, assignability assertions inspect the runtime type instead.
/// </remarks>
public new TypeIsAssignableToAssertion<TTarget> IsAssignableTo<TTarget>()
{
Context.ExpressionBuilder.Append($".IsAssignableTo<{typeof(TTarget).Name}>()");
return new TypeIsAssignableToAssertion<TTarget>(Context);
}

/// <summary>
/// Asserts that the represented type is not assignable to <typeparamref name="TTarget"/>.
/// </summary>
/// <remarks>
/// Represented-type semantics apply only when this is the first assertion after <c>Assert.That(type)</c>.
/// After <c>.And</c> or <c>.Or</c>, assignability assertions inspect the runtime type instead.
/// </remarks>
public new IsNotAssignableToAssertion<TTarget, Type> IsNotAssignableTo<TTarget>()
{
Context.ExpressionBuilder.Append($".IsNotAssignableTo<{typeof(TTarget).Name}>()");
return new IsNotAssignableToAssertion<TTarget, Type>(Context, useRepresentedType: true);
}

/// <summary>
/// Asserts that <typeparamref name="TSource"/> is assignable to the represented type.
/// </summary>
/// <remarks>
/// Represented-type semantics apply only when this is the first assertion after <c>Assert.That(type)</c>.
/// After <c>.And</c> or <c>.Or</c>, assignability assertions inspect the runtime type instead.
/// </remarks>
public new IsAssignableFromAssertion<TSource, Type> IsAssignableFrom<TSource>()
{
Context.ExpressionBuilder.Append($".IsAssignableFrom<{typeof(TSource).Name}>()");
return new IsAssignableFromAssertion<TSource, Type>(Context, useRepresentedType: true);
}

/// <summary>
/// Asserts that <typeparamref name="TSource"/> is not assignable to the represented type.
/// </summary>
/// <remarks>
/// Represented-type semantics apply only when this is the first assertion after <c>Assert.That(type)</c>.
/// After <c>.And</c> or <c>.Or</c>, assignability assertions inspect the runtime type instead.
/// </remarks>
public new IsNotAssignableFromAssertion<TSource, Type> IsNotAssignableFrom<TSource>()
{
Context.ExpressionBuilder.Append($".IsNotAssignableFrom<{typeof(TSource).Name}>()");
return new IsNotAssignableFromAssertion<TSource, Type>(Context, useRepresentedType: true);
}
}
110 changes: 109 additions & 1 deletion tests/TUnit.Assertions.Tests/TypeAssertionTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
using System.Reflection;
using TUnit.Assertions.Core;
using TUnit.Assertions.Extensions;
using TUnit.Assertions.Extensions;
using TUnit.Assertions.Sources;

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 +464,109 @@ 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_TypeInfo_GenericAssignability_UsesRepresentedType()
{
System.Reflection.TypeInfo animalType = typeof(Animal).GetTypeInfo();
System.Reflection.TypeInfo dogType = typeof(Dog).GetTypeInfo();

await Assert.That(dogType).IsAssignableTo<Animal>();
await Assert.That(dogType).IsNotAssignableTo<Type>();
await Assert.That(animalType).IsAssignableFrom<Dog>();
await Assert.That(dogType).IsNotAssignableFrom<Animal>();
}

[Test]
public async Task Test_Type_IsAssignableTo_GenericSource_RetainsRuntimeTypeSemantics()
{
IAssertionSource<Type> source = new TypeValueAssertion(typeof(Dog), null);

await Assert.That(async () => await source.IsAssignableTo<Animal>())
.Throws<AssertionException>();
}

[Test]
public async Task Test_Type_IsAssignableTo_AfterAnd_RetainsRuntimeTypeSemantics()
{
var action = async () => await Assert.That(typeof(Dog))
.IsClass()
.And.IsAssignableTo<Animal>();

await Assert.That(action).Throws<AssertionException>();
}

[Test]
public async Task Test_Type_OtherGenericSources_RetainRuntimeTypeSemantics()
{
IAssertionSource<Type> source = new TypeValueAssertion(typeof(Animal), null);

await source.IsNotAssignableTo<Animal>();
await Assert.That(async () => await source.IsAssignableFrom<Dog>())
.Throws<AssertionException>();
await source.IsNotAssignableFrom<Dog>();
}

[Test]
public async Task Test_Type_DirectGenericAssertions_UseRepresentedType()
{
await Assert.That(typeof(Animal)).IsNotAssignableTo<Type>();
await Assert.That(typeof(Animal)).IsAssignableFrom<Dog>();
await Assert.That(async () => await Assert.That(typeof(Animal)).IsNotAssignableFrom<Dog>())
.Throws<AssertionException>();
}

[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));
}

[Test]
public async Task Test_Type_IsAssignableTo_NullRuntimeType_FailsAssertion()
{
var action = async () => await Assert.That(typeof(Dog)).IsAssignableTo(null!);

var exception = await Assert.That(action).Throws<AssertionException>();
await Assert.That(exception.Message).Contains("expected type was null");
}

[Test]
public async Task Test_Type_IsAssignableFrom_NullRuntimeType_FailsAssertion()
{
var action = async () => await Assert.That(typeof(Animal)).IsAssignableFrom(null!);

var exception = await Assert.That(action).Throws<AssertionException>();
await Assert.That(exception.Message).Contains("source type was null");
}

#if NET5_0_OR_GREATER
// IsByRefLike / IsNotByRefLike (NET5+)
[Test]
Expand Down
Loading
Loading