nunit
nunit copied to clipboard
`NullReferenceException` when printing error messages for `IEnumerable`s for which `GetEnumerator()` returns `null`
When an assertion fails NUnit try to print the state of actual and expected objects. In certain cases (like the one below), it fails with a NullReferenceException
when it tries to get the enumerator of an IEnumerable
object whose GetEnumerator()
returns null
. This is particularly interesting because the issue might go unnoticed during development because it can only be seen when the test in question fails (which might be years later).
As a workaround for the exception one can define a custom formatter for the type. But in order to do so, you need to know that there is an issue in the first place.
The provided example is not quiet as minimal as I would wish it to be, but simpler cases do not trigger the bug, presumably because of #3949 and #4140 or similar issues.
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using NUnit.Framework;
namespace Test;
[TestFixture]
public sealed class Tests
{
[Test]
public void Equality()
{
// TestContext.AddFormatter<SomeEnumerable>(e => ((SomeEnumerable)e).Name);
var actual = Enumerable.Empty<SomeEnumerable>();
var expected = new[] { new SomeEnumerable("First"), new SomeEnumerable("Second") };
Assert.That(actual, Is.EqualTo(expected));
}
}
[SuppressMessage("Design", "CA1010")]
[SuppressMessage("Naming", "CA1710")]
public record SomeEnumerable : IEnumerable
{
public SomeEnumerable(string name) => Name = name;
public IEnumerator GetEnumerator() => null;
public string Name { get; }
}
System.NullReferenceException : Object reference not set to an instance of an object.
at NUnit.Framework.Constraints.MsgUtils.FormatCollection(IEnumerable collection, Int64 start, Int32 max)
at NUnit.Framework.Constraints.MsgUtils.<>c__DisplayClass16_7.<.cctor>b__20(Object val)
at NUnit.Framework.Constraints.MsgUtils.<>c__DisplayClass16_8.<.cctor>b__21(Object val)
at NUnit.Framework.Constraints.MsgUtils.<>c__DisplayClass16_9.<.cctor>b__22(Object val)
at NUnit.Framework.Constraints.MsgUtils.<>c__DisplayClass16_10.<.cctor>b__23(Object val)
at NUnit.Framework.Constraints.MsgUtils.<>c__DisplayClass16_11.<.cctor>b__24(Object val)
at NUnit.Framework.Constraints.MsgUtils.<>c__DisplayClass16_12.<.cctor>b__25(Object val)
at NUnit.Framework.Internal.TestExecutionContext.<>c.<.ctor>b__7_0(Object val)
at NUnit.Framework.Constraints.MsgUtils.FormatValue(Object val)
at NUnit.Framework.Constraints.EqualConstraintResult.DisplayEnumerableDifferences(MessageWriter writer, IEnumerable expected, IEnumerable actual, Int32 depth)
at NUnit.Framework.Constraints.EqualConstraintResult.DisplayDifferences(MessageWriter writer, Object expected, Object actual, Int32 depth)
at NUnit.Framework.Constraints.EqualConstraintResult.WriteMessageTo(MessageWriter writer)System.NullReferenceException : Object reference not set to an instance of an object.
at NUnit.Framework.Constraints.MsgUtils.FormatCollection(IEnumerable collection, Int64 start, Int32 max)
at NUnit.Framework.Constraints.MsgUtils.<>c__DisplayClass16_7.<.cctor>b__20(Object val)
at NUnit.Framework.Constraints.MsgUtils.<>c__DisplayClass16_8.<.cctor>b__21(Object val)
at NUnit.Framework.Constraints.MsgUtils.<>c__DisplayClass16_9.<.cctor>b__22(Object val)
at NUnit.Framework.Constraints.MsgUtils.<>c__DisplayClass16_10.<.cctor>b__23(Object val)
at NUnit.Framework.Constraints.MsgUtils.<>c__DisplayClass16_11.<.cctor>b__24(Object val)
at NUnit.Framework.Constraints.MsgUtils.<>c__DisplayClass16_12.<.cctor>b__25(Object val)
at NUnit.Framework.Internal.TestExecutionContext.<>c.<.ctor>b__7_0(Object val)
at NUnit.Framework.Constraints.MsgUtils.FormatValue(Object val)
at NUnit.Framework.Constraints.MsgUtils.FormatCollection(IEnumerable collection, Int64 start, Int32 max)
at NUnit.Framework.Internal.TextMessageWriter.WriteCollectionElements(IEnumerable collection, Int64 start, Int32 max)
at NUnit.Framework.Constraints.EqualConstraintResult.DisplayCollectionDifferences(MessageWriter writer, ICollection expected, ICollection actual, Int32 depth)
at NUnit.Framework.Constraints.EqualConstraintResult.DisplayDifferences(MessageWriter writer, Object expected, Object actual, Int32 depth)
at NUnit.Framework.Constraints.EqualConstraintResult.WriteMessageTo(MessageWriter writer)
at NUnit.Framework.Assert.ReportFailure(ConstraintResult result, String message, Object[] args)
at NUnit.Framework.Assert.That[TActual](TActual actual, IResolveConstraint expression, String message, Object[] args)
at NUnit.Framework.Assert.That[TActual](TActual actual, IResolveConstraint expression)
at Test.Tests.Equality()
This is tightly related to #4150, but I opted to keep the issue separate in case you decide the change the behavior in one case, but not the other.
NUnit 3.13.3
Again, the real world use case is a mock that implicitly implements IEnumerable
.
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Moq;
using NUnit.Framework;
namespace Test;
[TestFixture]
public sealed class Tests
{
[Test]
public void Equality()
{
// TestContext.AddFormatter<ISomeEnumerable>(e => Mock.Get((ISomeEnumerable)e).Name);
var mockedEnumerable1 = new Mock<ISomeEnumerable> { Name = "First" }.Object;
var mockedEnumerable2 = new Mock<ISomeEnumerable> { Name = "Second" }.Object;
var actual = Enumerable.Empty<ISomeEnumerable>();
var expected = new[] { mockedEnumerable1, mockedEnumerable2 };
Assert.That(actual, Is.EqualTo(expected));
}
}
[SuppressMessage("Design", "CA1010")]
[SuppressMessage("Naming", "CA1710")]
public interface ISomeEnumerable : IEnumerable
{
}