Skip to content

BeEquivalentTo with IgnoringCyclicReferences does not detect that actual model does not have cyclic reference. #2787

@renzefeitsma-bt

Description

@renzefeitsma-bt

Description

When asserting Should().BeEquivalentTo on a model that has 2-way references, with the IgnoringCyclicReferences, the assertion doesn't detect that the actual model objects don't have the cyclic reference, if the expected model has a cyclic or back-reference. While the equivalency assertion the other way around does detect the difference.

See the code example, but in short:

  • Expected model: Parent references Child, and Child references parent
  • Actual model: Parent references Child, but Child doesn't reference parent.

The weird thing is, when doing
actualParent.Should().BeEquivalentTo(expectedParent, options => options.IgnoringCyclicReferences())
it does not detect the difference between both models,
while when doing
expectedParent.Should().BeEquivalentTo(actualParent, options => options.IgnoringCyclicReferences())
it does detect the difference.
I would expect that equivalency works both ways.

Reproduction Steps

using FluentAssertions;
using Xunit;
using Xunit.Sdk;

namespace FluentAssertionsExperiment;

public class EquivalencyOptionsIgnoreCyclicTestsSimplified
{
    [Fact]
    public void IgnoringCyclicReferences_BackReferenceIsNull_ActualEquivalentToExpected_DoesNotThrow()
    {
        // Arrange expected model
        var expectedChild = new Child() {Id = 1};
        var expectedParent = new Parent(){ Id = 100, Children = new List<Child>(){expectedChild}};
        expectedChild.Parent = expectedParent;

        // Arrange actual model
        var actualChild = new Child() { Id = 1 };
        var actualParent = new Parent() { Id = 100, Children = new List<Child>() { actualChild } };
        //actualChild.Parent = actualParent; // => this is cleary different between both models

        // Assert actual model be equivalent to expected model => Does not throw.
        var action1 = () => actualParent.Should().BeEquivalentTo(expectedParent, options => options.IgnoringCyclicReferences());
        action1.Should().NotThrow();

        // Assert expected model be equivalent to actual model => Does throw.
        var action2 = () => expectedParent.Should().BeEquivalentTo(actualParent, options => options.IgnoringCyclicReferences());
        action2.Should().Throw<XunitException>();
    }

    private class Parent
    {
        public int Id { get; set; }
        public List<Child> Children { get; set; }
    }

    private class Child
    {
        public int Id { get; set; }
        public Parent Parent { get; set; }
    }
}

Expected behavior

Should.BeEquivalent() should throw in both directions.

Actual behavior

Should.BeEquivalent() throws only when the expected model doesn't have the cyclic reference, and does NOT throw when the expected model has the cyclic reference.

Regression?

Don't know.

Known Workarounds

Do the Should().BeEquivalent both ways in your test.

Configuration

Tested with .Net 8, and version 6.12.1 of FluentAssertions.

Other information

I tested this with a few different setups:

  • Parent has collection of Children as reference
  • Parent has single item reference to Child
  • Similar behavior on the 2nd level from the root (added GrandChild level)
  • Setting the Child.Parent property to a different Parent, instead of leaving empty.
    I can provide testcode for these scenarios if needed.

I am willing to help test solutions, or doing some code-analysis, if I get some pointers to where to start.

Are you willing to help with a pull-request?

No

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions