Nulls, types and ranges

edit

Conditions and null values

static string? FindUser(int id) => id == 1 ? "Alice" : null;

Assert.Null(FindUser(2));

var user = Assert.NotNull(FindUser(1));     // returns the value as a non-nullable string
Assert.Equal("ALICE", user.ToUpperInvariant());

Assert.True(char.IsUpper(user[0]));
Assert.False(string.IsNullOrWhiteSpace(user));
  • True and False check a bool or a bool?. Prefer a specialized assertion when there is one: the failure message of Assert.True(list.Contains(item)) only says that the condition is false, while Assert.Contains(item, list) shows the list. The analyzer suggests the specialized assertion.
  • Null and NotNull accept references and nullable value types. NotNull returns the value with its non-nullable type, so the rest of the test needs no !.
  • A non-nullable value type is never null: the analyzer reports Null and NotNull on one as an error (MFAS0008, MFAS0009).

Types

object value = new List<int> { 1, 2 };

var list = Assert.IsType<List<int>>(value);                 // exactly this type, and returned as it
Assert.HasCount(2, list);
Assert.IsNotType<int[]>(value);

var items = Assert.IsAssignableTo<IEnumerable<int>>(value); // value is IEnumerable<int>
Assert.Contains(2, items);
Assert.IsNotAssignableTo<string>(value);
AssertionPasses whenReturns
IsType<T>(value)value is exactly of type Tvalue as a T
IsNotType<T>(value)value is not exactly of type T
IsAssignableTo<T>(value)value is T: the type, a derived type or an implemented interfacevalue as a T
IsNotAssignableTo<T>(value)value is not T
  • A null value fails IsType and IsAssignableTo; after them, the compiler knows the value is not null.
  • A boxed int is considered to be exactly of type int?, as boxing an int? produces a boxed int.
  • IsAssignableTo matches what value is T matches, including COM objects and IDynamicInterfaceCastable implementations.
  • Each assertion has an overload that takes a Type instead of a type parameter: Assert.IsType(typeof(List<int>), value).
  • No value is exactly of an abstract or static type: the analyzer reports IsType with one as an error (MFAS0003).

Ranges

var percentage = 42;

Assert.InRange(percentage, 0, 100);         // the value comes first, then the inclusive bounds
Assert.NotInRange(percentage, 50, 100);

Assert.InRange("banana", "apple", "cherry", StringComparer.Ordinal);
Assert.NotInRange(double.NaN, double.MinValue, double.MaxValue);
  • InRange(actual, low, high) passes when low <= actual <= high: both bounds are included. The value under test comes first, as there is no single expected value.
  • The values are compared with Comparer<T>.Default, or with the IComparer<T> you pass.
  • Without a comparer, NaN and a null Nullable<T>, as the value or as a bound, are never in range, like low <= value && value <= high.
  • NotInRange passes when the value is outside the range.

InRange and NotInRange also accept a ref struct type that implements IComparable<T>:

var temperature = new Temperature(20);
Assert.InRange(temperature, new Temperature(0), new Temperature(25));   // compared with IComparable<Temperature>.CompareTo
Assert.NotInRange(temperature, new Temperature(21), new Temperature(25));

The failure message shows the expressions of the bounds and the type of the values, as a ref struct value cannot be displayed. See Compare ref structs for the Temperature type.