Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use NUnit's EqualTo().Within() constraint with a custom data type?

I love NUnit's constraint-based API. I often use floating point comparison like this:

double d = foo.SomeComputedProperty;

Assert.That(d, Is.EqualTo(42.0).Within(0.001));

Very readable!

However, if I have a custom class whose equality depends on floating point comparison:

class Coord
{
  Coord(double radius, double radians)
  {
    this.Radius = radius;
    this.Radians = radians;
  }

  double Radius { get; }
  double Radians { get; }

  public override bool Equals(Object obj)
  {
    Coord c = obj as Coord;
    if (obj == null || c == null) return false;

    return c.Radians == this.Radians && c.Radius == this.Radius;
  }
}

I would like to write my tests like this:

Coord reference = new Coord(1.0, 3.14);

// test another Coord for near-equality to a reference Coord:
Assert.That(testCoord, Is.EqualTo(reference).Within(0.001));

Is it at all possible to use NUnit like this?

like image 256
kdbanman Avatar asked Jul 31 '15 18:07

kdbanman


2 Answers

The following is written for NUnit 3

Using the System.Numerics.Complex class as a simple example, I can write a test

Assert.That(z1, Is.EqualTo(z2).Using<Complex>(NearlyEqual));

with this comparison function:

internal static bool NearlyEqual(Complex z1, Complex z2)
{
    return Math.Abs(z1.Real - z2.Real) < 1e-10 &&
           Math.Abs(z1.Imaginary - z2.Imaginary) < 1e-10;
}

This gives you the Comparer that was mentioned in juharr's comment, and achieves the basics of what you asked. It doesn't let me parameterise the tolerance, but it's close.

In order to complete the requirement, I would instead want to write the test as

Assert.That(z1, Is.EqualTo(z2).Within(new Complex(1e-10, 1e-10)));

but, as noted in the question, it's not as easy. It requires a new constraint extension method

public static class ComplexTestExtensions
{
    public static ComplexEqualConstraint WithinZ(this EqualConstraint constraint, Complex tolerance)
    {
        return new ComplexEqualConstraint(constraint) { Tolerance = tolerance };
    }
}

and then write the custom constraint along these lines:

public class ComplexEqualConstraint
    : EqualConstraint
{
    public ComplexEqualConstraint(EqualConstraint that)
        : base(that)
    {
    }

    public override ConstraintResult ApplyTo<TActual>(TActual actual)
    {
        bool success = false;
        if (actual is Complex z1)
        {
            Complex z2 = (Complex)this.Arguments[0];
            success = Math.Abs(z1.Real - z1.Real) < Tolerance.Real &&
                      Math.Abs(z1.Imaginary - z2.Imaginary) < Tolerance.Imaginary;
        }
        return new ConstraintResult(this, actual, success);
    }

    public new Complex Tolerance
    {
        get;
        set;
    } = Complex.Zero;
}
like image 97
ClickRick Avatar answered Nov 12 '22 08:11

ClickRick


Based on ClickRick's answer:

If you need something that works with NUnit 2.6 (which for instance Unity's test framework is based on):

public static class ColorTestExtensions
{
    public static EqualConstraint WithinManhattanDistance(this EqualConstraint constraint, float tolerance)
    {
        return new ColorEqualConstraint((Color) constraint.Arguments[0]).Within(tolerance);
    }

    private class ColorEqualConstraint : EqualConstraint
    {
        public ColorEqualConstraint(Color expected) : base(expected) {}

        public override ConstraintResult ApplyTo(object actual)
        {
            if (!(actual is Color c1)) return new ConstraintResult(this, actual, false);

            var c2 = (Color) Arguments[0];
            var tolerance = (float) Tolerance.Value;
            var success = ColorExtensions.ManhattanDistance(c1, c2) < tolerance;
            return new ConstraintResult(this, actual, success);
        }
    }
}

This lets you write:

Color color = GetPixel(0,0);
Assert.That(color, Is.EqualTo(Color.red).WithinManhattanDistance(0.1f);
like image 32
bobbaluba Avatar answered Nov 12 '22 06:11

bobbaluba