Skip to content

Make T nullable in ComparableTypeAssertions methods that are use IComparable<T>.CompareTo(...) #2874

@Zeruxky

Description

@Zeruxky

Background and motivation

The IComparable<T> interface offers a single method CompareTo(...) that accepts a nullable T (T?).

When we want to write a test for the case, where we check, if an object of class X is greater than null, then we need to use the null-forgiving operator (!) to avoid warnings. The class X can be implemented as following:

public class X : IComparable<X>
{
    public override int CompareTo(X? other)
    {
        if (ReferenceEquals(this, other))
        {
          return 0;
        }

        if (other is null)
        {
          return 1;
        }

        return this.value.CompareTo(other.value);
    }

    public int CompareTo(object? obj)
    {
        if (obj is null)
        {
            return 1;
        }

        if (ReferenceEquals(this, obj))
        {
            return 0;
        }

        return obj is X other
            ? this.CompareTo(other)
            : throw new ArgumentException($"Object must be of type {nameof(X)}");
    }
}

An associated test using the current version of FluentAssertions must be implemented as following, to avoid warnings by the compiler:

public class XTest
{
    public void Is_greater_than_null()
    {
        // Arrange
        var x = new X(1);

        // Assert
        x.Should().BeGreaterThan(null!); // The ! (null-forgiving) operator avoids the warning
    }
}

Since the CompareTo(...) method of IComparable<T> accepts a nullable T, with T the type to compare, it would be a better fit to support nullable values in the following methods:

  • BeGreaterThan(...) / BeGreaterThanOrEqualTo(...)
  • BeLessThan(...) / BeLessThanOrEqualTo(...)
  • BeRankedEquallyTo(...) / NotBeRankedEquallyTo(...)
  • BeInRange(...) / NotBeInRange(...)

This change enables to write assertions without using the null-forgiving operator (!) and doesn't change the functionality of existing assertions based on IComparable<T>. So the code within the unit test is more 'cleaner'.

API Proposal

public class ComparableTypeAssertions<T, TAssertions> : ReferenceTypeAssertions<IComparable<T>, TAssertions>
    where TAssertions : ComparableTypeAssertions<T, TAssertions>
{
-    public AndConstraint<TAssertions> BeRankedEquallyTo(T expected, string because = "", params object[] becauseArgs);
+    public AndConstraint<TAssertions> BeRankedEquallyTo(T? expected, string because = "", params object[] becauseArgs);

-    public AndConstraint<TAssertions> NotBeRankedEquallyTo(T expected, string because = "", params object[] becauseArgs);
+    public AndConstraint<TAssertions> NotBeRankedEquallyTo(T? expected, string because = "", params object[] becauseArgs);

-    public AndConstraint<TAssertions> BeLessThan(T expected, string because = "", params object[] becauseArgs);
+    public AndConstraint<TAssertions> BeLessThan(T? expected, string because = "", params object[] becauseArgs);

-    public AndConstraint<TAssertions> BeLessThanOrEqualTo(T expected, string because = "", params object[] becauseArgs);
+    public AndConstraint<TAssertions> BeLessThanOrEqualTo(T? expected, string because = "", params object[] becauseArgs);

-    public AndConstraint<TAssertions> BeGreaterThan(T expected, string because = "", params object[] becauseArgs);
+    public AndConstraint<TAssertions> BeGreaterThan(T? expected, string because = "", params object[] becauseArgs);

-    public AndConstraint<TAssertions> BeGreaterThanOrEqualTo(T expected, string because = "", params object[] becauseArgs);
+    public AndConstraint<TAssertions> BeGreaterThanOrEqualTo(T? expected, string because = "", params object[] becauseArgs);

-    public AndConstraint<TAssertions> BeInRange(T minimumValue, T maximumValue, string because = "", params object[] becauseArgs);
+    public AndConstraint<TAssertions> BeInRange(T? minimumValue, T? maximumValue, string because = "", params object[] becauseArgs);

-    public AndConstraint<TAssertions> NotBeInRange(T minimumValue, T maximumValue, string because = "", params object[] becauseArgs);
+    public AndConstraint<TAssertions> NotBeInRange(T? minimumValue, T? maximumValue, string because = "", params object[] becauseArgs);
}

API Usage

var one = 1;
one.Should().BeGreaterThan(null);
one.Should().NotBeLessThan(null);
one.Should().BeGreaterThanOrEqualTo(null);
one.Should().NotBeLessThanOrEqualTo(null);
one.Should().NotBeRankedEquallyTo(null);
one.Should().NotBeInRange(null, null);

Alternative Designs

No response

Risks

None. Should work with legacy code, where nullable reference types are not enabled explicit.

Are you willing to help with a proof-of-concept (as PR in that or a separate repo) first and as pull-request later on?

Yes, please assign this issue to me.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationenhancement

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions