Skip to content

Accessing .Which if the constraint fails causes the rest of the code in AssertionScope to be skipped (immediate throw) #2664

@rbeurskens

Description

@rbeurskens

Description

If I want to check for multiple conditions that I all want to report, and one happens to be a check on the existence of an item in a collection (and using .Which for additional checking in the item itself if it does exist), I do not expect .Which to throw if the Which Constraint<,> failed, but store the failure in the AssertionScope and ignore the constraint on .Which instead.

Reproduction Steps

// given similar API:
public class MyDataReader : IDataReader
{
    IEnumerable<IField> Fields { get; set;}
    IField GetField(string name) => Fields.SingleOrDefault(f=>f.ColumnName == name);
    // (...)
 }
public class Field : IField
{
    Field (string name, object value)
    {
        ColumnName = name;
        DataValue = value;
    }
    string ColumnName { get; }
    object DataValue { get; }
}

// Arrange
// values are both incorrect (swapped), column-/fieldname "Name" is different from expected "LastName"
var myRecord = new Field[] { new("FirstName", "Doe"), new("Name", "John") };
var dr = new MyDataReader { Fields = myRecord };
string input = "MyString";

// Act
bool result = dr.Read();

// Assert
result.Should().BeTrue();
using (new AssertionScope)
{
    // Expected "LastName", but actual is "Name"
    dr.Fields.Should().ContainSingle(field=>field.ColumnName=="LastName").Which.DataValue.Should().Be("Doe");
    // Because .Which above throws, the below code is not executed, but its results are expected to be included in the output.  
    dr.Fields.Should().ContainSingle(field=>field.ColumnName=="FirstName").Which.DataValue.Should().Be("John"); // <== Mismatches found here are not in the output
}

Expected behavior

Expected dr.Fields to contain a single item matching (field.ColumnName == "LastName"), but no such item was found.
Expected dr.Fields to be "John" with a length of 4, but "Doe" has a length of 3, differs near "Doe" (index 0).

// actual, the subject description for the check on .Which ('dr.Field') could also be improved ('dr.Fields.Item.DataValue'(?)), but I think there is already another issue for that. (related to #1502 and/or #2253 ?)

Actual behavior

Expected dr.Fields to contain a single item matching (field.ColumnName == "LastName"), but no such item was found.

Regression?

No response

Known Workarounds

using (new AssertionScope)
{
    // Expected "LastName", but actual is "Name"
    using (var inner = new AssertionScope()) // isolated inner scope (in case there is an assertion added above that resulted in a failure)
        if (dr.Fields.Should().ContainSingle(field=>field.ColumnName=="LastName") is var constraint && !inner.HasFailures())
            constraint.Which.DataValue.Should().Be("Doe");
     // Because .Which above is now only accessed if there is no failure, it will not throw, so the below code is executed, and results are included in the output.
     // Has no effect here now, but there may be assertions added below in the future
     using (var inner = new AssertionScope()) // isolated inner scope (so we now hasFailure would always be caused by this assertion)
        if (dr.Fields.Should().ContainSingle(field=>field.ColumnName=="FirstName") is var constraint && !inner.HasFailures())
            constraint.Which.DataValue.Should().Be("John"); // <== Mismatches found here are now in the output
}

or

using (var scope = new AssertionScope)
{
    // using extension (A)
    if (scope.IsSuccessful(() => dr.Fields.Should().ContainSingle(field => field.ColumnName == "LastName")
        // dr.GetField("LastName").DataValue.Should().Be("Doe");
        dr.Fields.Single(field => field.ColumnName == "LastName").DataValue.Should().Be("Doe");
    // (...)
    // or (B)
    scope.IfSuccessful(() => dr.Fields.Should().ContainSingle(field => field.ColumnName == "LastName")
        field => field.DataValue.Should().Be("Doe"));
        // _ => dr.Fields.Single(field => field.ColumnName == "LastName").DataValue.Should().Be("Doe"); // would work too and adds some more context to the output.
    // (...)
}

public static class FluentAssertionExtensions
{
    // extending on AssertionScope to reduce chance to have them apply in code where they are not relevant

    // alternative A
    public static bool IsSuccessful(this IAssertionScope _ , Action evaluateConstraint)
    {
        using var scope = new AssertionScope();
        evaluateConstraint();
        return !scope.HasFailures();
    }
    // alternative B
    public static void IfSuccessful<TParent, T>(this IAssertionScope _ , Func<AndWhichConstraint<TParent,T>> constraintFunc, Action<T> evaluateItem)
    {
        using var scope = new AssertionScope();
        var constraint = constraintFunc();
        if (!scope.HasFailures())
             evaluateItem(constraint.Which);
    }
    // alternative C (quick and dirty - use of exception for control flow)
    public static void IfSuccessful<TParent, T>(this AndWhichConstraint<TParent, T> constraint, Action<T> code)
    {
        T subject;
        try { subject = constraint.Which!; }
        catch (Exception) { return; }
        code(subject);
    }
}

Configuration

No response

Other information

No response

Are you willing to help with a pull-request?

No

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions