Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: Allow using the working comparison even when JTokens are nested #61

Open
chrischu opened this issue Feb 10, 2022 · 8 comments

Comments

@chrischu
Copy link

chrischu commented Feb 10, 2022

Currently when running the following code:

var d = new Dictionary<string, JToken> { ["a"] = 5 };
var e = new Dictionary<string, JToken> { ["a"] = 7 };
d.Should().BeEquivalentTo(e);

The difference between the values is not detected (i.e. no assertion error is raised). It would be cool if this NuGet package would offer some kind of extension method on EquivalencyAssertionOptions (or similar) to allow me to make the comparison work correctly.

@dennisdoomen
Copy link
Member

It seems what you're looking for is a combination of what FluentAssertions and FluentAssertions.Json does. Something that plugs in support for JToken (which depends on either Newtonsoft or Microsoft's library).

@chrischu
Copy link
Author

Yes that is what I'm looking for, I assumed this project would be the right place for it given its name and the fact that it already combines FluentAssertion with Newtonsoft's JToken, albeit only for top-level comparisons :).

@jnyrup
Copy link
Member

jnyrup commented Feb 11, 2022

You can extend the equivalency comparisons with the BeEquivalentTo from FluentAssertions.Json to add the special handling of JToken.
Here are two examples depending on whether you want to to it for a single test or for all tests.

[TestMethod]
public void LocalOptions()
{
    var d = new Dictionary<string, JToken> { ["a"] = 5 };
    var e = new Dictionary<string, JToken> { ["a"] = 7 };
    d.Should().BeEquivalentTo(e, opt => opt
        .Using<JToken>(ctx => ctx.Subject.Should().BeEquivalentTo(ctx.Expectation))
        .WhenTypeIs<JToken>()
    );
}

[TestMethod]
public void GlobalOptions()
{
    AssertionOptions.AssertEquivalencyUsing(e => e
        .Using<JToken>(ctx => ctx.Subject.Should().BeEquivalentTo(ctx.Expectation))
        .WhenTypeIs<JToken>()
    );

    var d = new Dictionary<string, JToken> { ["a"] = 5 };
    var e = new Dictionary<string, JToken> { ["a"] = 7 };
    d.Should().BeEquivalentTo(e);
}

@dennisdoomen
Copy link
Member

Indeed. But I was playing with the idea of having some kind of automatic configuration where adding a reference to the JSON package would automatically add the relevant IEquivalencyStep in the background.

@chrischu
Copy link
Author

@jnyrup Thanks for the idea, I did not consider that. This will work as a workaround until there might be a better solution :).

@chrischu
Copy link
Author

I tried the solution proposed by @jnyrup and it works, unfortunately it kind of breaks the nice display of assertion errors we usually get with Should().BeEquivalentTo(), e.g. if we compare an object with multiple JToken instances and two of them are not as expected we get the following error which does not offer any information on which property held the JToken instance that was not as expected:
image

Is there any way to solve this problem (by building a more complex IEquivalencyStep or something along those lines)?

@dennisdoomen
Copy link
Member

I think the limitation is in FluentAssertions.Json. It doesn't use the {context} construct in its implementation of BeEquivalentTo. See https://github.com/fluentassertions/fluentassertions.json/blob/master/Src/FluentAssertions.Json/JTokenAssertions.cs#L90

@jnyrup
Copy link
Member

jnyrup commented Feb 16, 2022

If you're okay with using a hacky workaround, which might break at any time.

class JTokenEquivalencyStep : EquivalencyStep<JToken>
{
    protected override EquivalencyResult OnHandle(Comparands comparands, IEquivalencyValidationContext context, IEquivalencyValidator nestedValidator)
    {
        string message = null;
        using (var assertionScope = new AssertionScope())
        {
            var subject = comparands.Subject as JToken;
            var expectation = comparands.Expectation as JToken;

            subject.Should().BeEquivalentTo(expectation, context.Reason.FormattedMessage, context.Reason.Arguments);
            if (assertionScope.HasFailures())
            {
                message = assertionScope.Discard()[0].Replace("JSON document", "{context:JSON document}");
            }
        }

        if (message is not null)
        {
            AssertionScope.Current.FailWith(message);
        }

        return EquivalencyResult.AssertionCompleted;
    }
}
var subject = new { First = (JToken)1, Second = (JToken)2 };
var expectation = new { First = (JToken)3, Second = (JToken)4 };
subject.Should().BeEquivalentTo(expectation,
    options => options.Using(new JTokenEquivalencyStep()),
    "my {0} message", "failure");
Property root.First has a different value at $.
Actual document
1
was expected to be equivalent to
3
 because my failure message.
Property root.Second has a different value at $.
Actual document
2
was expected to be equivalent to
4
 because my failure message.

With configuration:
- Use declared types and members
- Compare enums by value
- Compare tuples by their properties
- Compare anonymous types by their properties
- Compare records by their members
- Match member by name (or throw)
- TestProject11.UnitTest1+JTokenEquivalencyStep
- Be strict about the order of items in byte arrays
- Without automatic conversion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants