1

I'm able to approximately compare two 2D rectangular arrays in Fluent Assertions like this:

float precision = 1e-5f;
float[,] expectedArray = new float[,] { { 3.1f, 4.5f}, { 2, 4} };
float[,] calculatedArray = new float[,] { { 3.09f, 4.49f}, { 2, 4} };

for (int y = 0; y < 2; ++y)
{
    for (int x = 0; x < 2; ++x)
    {
        calculatedArray[y,x].Should().BeApproximately(expectedArray[y,x], precision);
    }
}

but is there a cleaner way of achieving this (without the for loops)? For example, something in the same vein as this (which is for 1D arrays):

double[] source = { 10.01, 8.01, 6.01 };
double[] target = { 10.0, 8.0, 6.0  };

source.Should().Equal(target, (left, right) => Math.Abs(left-right) <= 0.01);

The above solution for a 1D array comes from the question: Fluent Assertions: Compare two numeric collections approximately

Community
  • 1
  • 1
Ayb4btu
  • 3,208
  • 5
  • 30
  • 42
  • If you're able to use `float[][]`, rather than `float[,]`, then you can expand on the linked answer to have something like: `expectedArray.Should().Equal(calculatedArray, (rowLeft, rowRight) => { rowLeft.Should().Equal(rowRight, (left, right) => Math.Abs(left - right) <= 0.01); return true; });` – forsvarir Apr 11 '16 at 13:02
  • @forsvarir The float[,] is being used to hold the elements of a matrix, so in this case my preference is to keep the rectangular array instead of using a jagged array. Though it is good to know that the overload can be used that way. – Ayb4btu Apr 11 '16 at 20:35

2 Answers2

2

There doesn't appear to be anything currently in the framework that supports this. If you don't want to have the loop in your tests, then one option would be for you to add your own extensions to cover this scenario.

There are two elements to this. The first is to add an extension method that adds the Should ability to 2D arrays:

public static class FluentExtensionMethods
{
    public static Generic2DArrayAssertions<T> Should<T>(this T[,] actualValue)
    {
        return new Generic2DArrayAssertions<T>(actualValue);
    }
}

You then need to implement the actual assertion class, which will contain the comparison loop:

public class Generic2DArrayAssertions<T> 
{
    T[,] _actual;

    public Generic2DArrayAssertions(T[,] actual)
    {
        _actual = actual;
    }

    public bool Equal(T[,] expected, Func<T,T, bool> func)
    {
        for (int i = 0; i < expected.Rank; i++)
            _actual.GetUpperBound(i).Should().Be(expected.GetUpperBound(i), 
                                                 "dimensions should match");

        for (int x = expected.GetLowerBound(0); x <= expected.GetUpperBound(0); x++)
        {
            for (int y = expected.GetLowerBound(1); y <= expected.GetUpperBound(1); y++)
            {
                func(_actual[x, y], expected[x, y])
                     .Should()
                     .BeTrue("'{2}' should equal '{3}' at element [{0},{1}]",
                      x, y, _actual[x,y], expected[x,y]);
            }
        }

        return true;
    }
}

You can then use it in your tests like other assertions:

calculatedArray.Should().Equal(expectedArray, 
                               (left,right)=> Math.Abs(left - right) <= 0.01);

I think your comment is asking how you go about testing the extension code I'm suggesting. The answer is, the same way you go about testing anything else, pass in values and validate the output. I've added some tests below (using Nunit) to cover some of the key scenarios. Some things to note, the data for the scenarios is incomplete (it seems out of scope and isn't that hard to generate). The tests are using a func of left == right, since the point is to test the extension, not the evaluation of the approximation.

[TestCaseSource("differentSizedScenarios")]
public void ShouldThrowIfDifferentSizes(float[,] actual, float[,] expected)
{
    Assert.Throws<AssertionException>(()=>actual.Should().Equal(expected, (l, r) => l == r)).Message.Should().Be(string.Format("Expected value to be {0} because dimensions should match, but found {1}.", expected.GetUpperBound(0), actual.GetUpperBound(0)));
}

[TestCaseSource("missMatchedScenarios")]
public void ShouldThrowIfMismatched(int[,] actual, int[,] expected, int actualVal, int expectedVal, string index)
{
    Assert.Throws<AssertionException>(()=>actual.Should().Equal(expected, (l, r) => l.Equals(r))).Message.Should().Be(string.Format("Expected True because '{0}' should equal '{1}' at element [{2}], but found False.", actualVal, expectedVal, index));
}

[Test]
public void ShouldPassOnMatched()
{
    var expected = new float[,] { { 3.1f, 4.5f }, { 2, 4 } };
    var actual = new float[,] { { 3.1f, 4.5f }, { 2, 4 } };
    actual.Should().Equal(expected, (l, r) => l.Equals(r));
}

static object[] differentSizedScenarios = 
{
    new object[] {
        new float[,] { { 3.1f, 4.5f }, { 2, 4 } },
        new float[,] { { 3.1f, 4.5f }, { 2, 4 }, {1,2} }
    },
    new object[] {
        new float[,] { { 3.1f, 4.5f }, { 2, 4 } },
        new float[,] { { 3.1f, 4.5f }}
    }
    // etc...
};
static object[] missMatchedScenarios = 
{
    new object[] {
        new int[,] { { 1, 2}, { 3, 4 } },
        new int[,] { { 11, 2}, { 3, 4 } }
        ,1, 11, "0,0"
    },
    new object[] {
        new int[,] { { 1, 2}, { 3, 14 } },
        new int[,] { { 1, 2}, { 3, 4 } }
        ,14, 4, "1,1"
    },
    // etc...
};
forsvarir
  • 10,749
  • 6
  • 46
  • 77
  • That's quite a nice solution. Now to be recursive, is there a way of writing unit tests to ensure this code is operating correctly? Or is this a case of having to manually checking the code (a fluentassertions fail case will exit `Generic2DArrayAssertions` before hitting the Assert in the unit test that calls it). – Ayb4btu Apr 12 '16 at 22:42
  • @Ayb4btu I think you're asking how to validate that the extension works, I've added some tests to validate the behaviour, if this isn't what you're after, then I'm not sure I understand your question. – forsvarir Apr 13 '16 at 10:17
  • Yeah, that was what I was asking. I didn't know how to capture and handle the fail case message, so having the .Message.Should().Be extension is quite neat. Thanks for the great answer. – Ayb4btu Apr 13 '16 at 20:30
1

I haven't fully tested this, but the following seems to work.

float precision = 0.1f; // Test passes with this level of precision.
//float precision = 0.01f; // Test fails with this level of precision.
float[,] expectedArray = new float[,] { { 3.1f, 4.5f }, { 2, 4 } };
float[,] calculatedArray = new float[,] { { 3.09f, 4.49f }, { 2, 4 } };

calculatedArray.Should().BeEquivalentTo(
    expectedArray,
    options => options
        .ComparingByValue<float>()
        .Using<float>(ctx => ctx.Subject.Should().BeApproximately(ctx.Expectation, precision))
        .WhenTypeIs<float>());
  • Welcome to Stack Overflow! Please note you are answering a very old and already answered question. Here is a guide on [How to Answer](http://stackoverflow.com/help/how-to-answer). – help-info.de Aug 14 '20 at 08:19