-
-
Notifications
You must be signed in to change notification settings - Fork 380
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
Object Graph Comparison - ShouldBeEquivalentTo(...) #411
Conversation
This is a great start. I also think we need to keep a list of seen objects to prevent infinite recursion bugs. Also worth reading the discussion at #245, there was some thought put into the API and it's feature set |
@JakeGinnivan Are you happy for this to be merged in its current state? If any issues or edge cases arise then we can tackle those separately but it seems like a great addition to Shouldly. |
@JakeGinnivan @josephwoodward This is awesome and I need it. Thanks for working on it! Would love to help - please let me know if I can. |
I'd really like to get this merged too @urig, though I'm currently struggling with migrating from project.json to .csproj (see this PR) - I think it's best we migrate to the new tooling before merging anymore changes. Any assistance there would always be welcome, it's almost there, just ran into some stumbling blocks. |
Hi. I have very little experience with Core at this time so I'm doubtful I can help. I will try though :) |
Sorry, I meant to pick back up on the comments made by @JakeGinnivan , but you know how life goes. :) I'll see if i can get back to it this week. |
@TaffarelJr Interested to know why you closed this? I was planning on merging it once the tooling changes were completed? |
Hmm, TBH I'm not sure how this got closed. I think in pulling down your latest changes and rebasing my stuff on top of it, I may have done something that cut the connection. I'm almost done with my changes though, and I'll be putting this back up in a new PR soon. |
@TaffarelJr That would be amazing, was looking forward to getting it merged as part of the official 3.0 release, it's a feature loads of people have been asking for :) |
This change involves change the datatype of the 'path' variable from IList<T> to IEnumerable<T> to get LINQ functionality (and maybe some better performance due to fewer passes through the lists).
This introduces the actual recursion into the comparison logic. Some refactoring of code is included to call attention to the places where the object graph path is modified.
This introduces a dictionary that keeps track of which objects have previously been (or are currently being) compared. Any new child comparisons will first check this dictionary; if the comparison has already been done between the current objects, then they are simply skipped. This should avoid infinite loop problems. The dictionary is keyed by the actual object being compared, and the values represent a list of objects that the key has been compared against.
Alright, I think I've got it now. It's a bit cleaner than it was before, and it now keeps track of which objects it's compared along the way. If it detects that it's already compared two objects (anywhere in the object graph), then it skips over them. This should keep us from having infinite loops. |
Awesome! Thanks so much for this @TaffarelJr ! |
As #432 has been merged (ref #411 (comment)), an chance this PR will be merged soon? |
As someone who has implemented something similar on our project, it would be very nice to see something like this included in Shouldly. However, avoiding infinite regress is a tricky business, since the object graph could conceivably grow to infinite depth.
(I discovered this problem because my own implementation blew up during similar conditions.) |
Right, it's probably not unbreakable code at this point. :) While I put in a reasonable effort to prevent infinite loops (and I welcome improvements), I don't think we can entirely prevent them here. And ultimately I think that's OK in this case, because it's not meant to be mission-critical code. If a developer uses |
Yeah I think you're right. While it's nice to have very robust code, it's probably a case of diminishing returns on investment in trying to solve an impossible problem. You can always construct infinite object graphs, and there's not much to be done about them. Another (silly) one:
|
One thing you could consider is try to detect problems and terminate the tests a bit more gracefully. For instance, since you are already keeping track of the property path, you could check if the path got unreasonably deep and fail the test at that point. Similarly you could choose some arbitrary cap on the number of elements in an
It's not perfect, but it tells the user what and where the problem was. |
a95a502
to
517e1c0
Compare
What is the status of this PR? When can we use such an essential functionality? |
Perhaps using a 3rd party solution is an option to get this implemented? For instance: https://github.com/GregFinzer/Compare-Net-Objects |
Any traction on this? It's the only thing Shouldly is missing imo! |
Watching this. Once it gets merged I will gladly switch to Shouldly. |
+1 |
I took a slightly different approach to this problem by splitting out the object graph into multiple shouldly assertions, see UnitTestCoder This is an example of what UnitTestCoder will automatically generate for you at runtime based on a populated model, copy the code back into your unit test and you have 100% coverage! model.Customers.Count().ShouldBe(2);
model.Customers[0].Name.ShouldBe("Apple");
model.Customers[0].Invoices.Count().ShouldBe(2);
model.Customers[0].Invoices[0].Amount.ShouldBe(123.45m);
model.Customers[0].Invoices[1].Amount.ShouldBe(6464.55m);
model.Customers[1].Name.ShouldBe("Facebook");
model.Customers[1].Invoices.Count().ShouldBe(1);
model.Customers[1].Invoices[0].Amount.ShouldBe(123.45m); It avoids circular references by keeping track of objects it has already seen and referring back to them by array index. |
@alansingfield I would not encourage you using this pattern, rather try this:
|
Going to be merging this one as soon as I've fixed #524 |
@alansingfield What @dgroh said. But also, wrap all your assertions inside an |
That looks fixed now? |
@josephwoodward ♫ All I want for Christmas is this PR merged. This PR merged. This PR merged. ♫ |
@martincostello @mikesigs Going to be merging this along with the ShouldMatchApproved xplat support in the new few days as a beta. Having been playing with this PR for a bit there's a number of edge cases that aren't right, but it's a good start. |
What is the hold up here? |
@thomas-parrish Been doing a bit of extra work around this, want this to be part of 4.0 (and hopefully get a beta out that we can iterate on for a few versions), it's good to merge now though and add the extra bits as a PR then get a beta out. |
Woot |
4.0.0-beta0001 still has no |
Missed it by that much! The 4.0.0-beta0001 tag was created on the commit juuuust before this one was merged. My guess is we'll get it on the next beta release. The fact that is was merged is a really good sign! |
Yes that' right, I've been making a few changes for it for the next beta release which should be out shortly. |
Bump :) |
3 years and counting... :\ |
I know, been sorting out the next release ready for this week so watch this space. It's really early implementation at the moment so will leave the the package as a beta for a while. |
Hello Joseph, I understand, It's harder than it looks... I've been updating UnitTestCoder.Shouldly to take account of more data types, enumerables and the like - but there's still plenty more it could do. You either have to limit yourself to the simplest POCO objects, or have to make decisions on which properties to follow and which to ignore. I took the route of adding a nofollow parameter so you can tell the system when to stop digging. I'm intending to turn this into a more fluent interface when I get the time. Are you intending to pass in another object of the same type, or allow any anonymous type with matching properties to work? MyModel obj = MakeModel();
// Compare to anonymous type
obj.ShouldBeEquivalentTo(new { Product = "ABC", Price = 234.56m }); MyModel obj = MakeModel();
// Compare to another instance; but - what if it's a non-constructable type?
MyModel comparison = new MyModel()
{
Product = "ABC",
Price = 234.56m
};
obj.ShouldBeEquivalentTo(comparison); The other issue is how to determine equivalence. For primitive types and strings, it's probably OK, but once you get circular referenced objects, how do you match from the comparison to the target? I took the approach of using the ordinal, a little like an MVC form submission. But this procludes you from testing plain IEnumerable<>, you need an IList<>. model.Customers[0].Invoices[1].Customer.ShouldBe(model.Customers[0]); I'll be interested to see what you've done and can if I can contribute anything useful I will! |
This change has now been released as part of the 4.0.0-beta0002 release which is now available on NuGet. This feature is still in its infancy and there are a few cases I've noticed it doesn't cater for, so I'll leave it in beta for a while so we can get some issues posted/fixed around it before we release a 4.0 RC The long term plan is to add some configuration options so you can cusrtomise how the comparison behaves. In the meantime give it a try, and if anyone notices any issues with this new feature then we'd appreciate it if you could report them by opening a new issue. |
@alansingfield Those are some interesting questions and to be honest, I don't know the answer right now. As it seems there's no right or wrong way for this to behave as default, I've been looking at a lot of other object comparison libraries out there to get an idea of what they have set as defaults. The long term plan (as mentioned above) is to provide the ability to configure how |
Not sure if this is late to the party OR totally wrong answer, here ... but .. for comparing two .net poco's, I just JSON serialize both the current poco and the expected poco and then compare two strings. This is comparing the data values of each poco, not memory addresses etc. e.g. (testing two pocos)
e.g. (two pocos are different)
|
I took a stab at implementing object graph comparison, noted in issue #370. I've done similar stuff in other projects, and thought I could knock it out. Let me know what you think.
For any two objects (actual & expected), the process is:
null
values.null
, then exit.null
, throw exception.ValueType
Object.Equals()
.String
StringComparison.Ordinal
.IEnumerable
(any list)Throughout the recursion process, it also keeps track of the current path, so if an error occurs somewhere in the object graph it appears in the error message. The format ends up looking like this: