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
Add support for Satisfy on ReferenceTypeAssertions #2597
base: develop
Are you sure you want to change the base?
Conversation
Qodana for .NETIt seems all right 👌 No new problems were found according to the checks applied 💡 Qodana analysis was run in the pull request mode: only the changed files were checked Contact Qodana teamContact us at qodana-support@jetbrains.com
|
Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.Satisfy.cs
Outdated
Show resolved
Hide resolved
Missing release notes? |
I didn't add it to the release notes yet as I'm unsure what version this will be in. @dennisdoomen I can see that we're at Alpha 3 now, but the release notes only contain Alpha 1. I've added the changes to the current section of the releases.md, but I think something might be wrong. Can you let me know if I need to change something? |
I think while on develop for the next version it doesn't really matter |
@ITaluone Alright, I added it the the most recent section. I guess it will be aligned when the final release goes out, however, it might be a bit confusing for people using the Alpha versions to see the release notes not reflecting the latest version. |
@dennisdoomen @ITaluone I believe I've addressed all comments now. |
failuresFromInspector = assertionScope.Discard(); | ||
} | ||
|
||
if (failuresFromInspector.Length > 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 Do we even need to do this? What if you just relied on the assertion scope to throw a combined message? What kind of information would you be missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know, I'll test it to see if it's necessary 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested the changes you suggested, and it looks like we will be losing the outer object context. There is also no indentation on the nested assertions, which I think is useful.
I don't know what is preferred though, however, this is how it's done in Match
, and I believe the result should be the same or similar, no?
Using a single assertion scope:
Expected person.Name to be the same string, but they differ at index 1:
↓ (actual)
"Buford Howard Tannen"
"Biff Tannen"
↑ (expected).
Expected person.Age to be 48, but found 87 (difference of 39).
Expected address.City to be "Hill Valley, San Diego County, California" with a length of 41, but "Hill Valley" has a length of 11, differs near "y" (index 10).
Using Discard
:
Expected complexDto to match inspector, but the inspector was not satisfied:
Expected dto.Person to match inspector, but the inspector was not satisfied:
Expected person.Name to be the same string, but they differ at index 1:
↓ (actual)
"Buford Howard Tannen"
"Biff Tannen"
↑ (expected).
Expected person.Age to be 48, but found 87 (difference of 39).
Expected dto.Address to match inspector, but the inspector was not satisfied:
Expected address.City to be "Hill Valley, San Diego County, California" with a length of 41, but "Hill Valley" has a length of 11, differs near "y" (index 10).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I rely solely on the assertion scope, I get this
Expected dto.Person to be the same string, but they differ at index 1:
↓ (actual)
"Buford Howard Tannen"
"Biff Tannen"
↑ (expected).
Expected dto.Person to be 48, but found 87 (difference of 39).
Expected dto.Address to be "Hill Valley, San Diego County, California" with a length of 41, but "Hill Valley" has a length of 11, differs near "y" (index 10).
I personally feel like this is much more readable without all the noisy mentioning of the inspector (which is the wrong term anyway), don't you agree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with the message is that it doesn't mention the failed nested property names:
complexDto.Person.Name
complexDto.Person.Age
complexDto.Address.City
If those exact property names aren't mentioned in the message, it's not helpful.
As it looks with the new changes, both the Name
and Age
properties gets merged into a single nonsensical error on the same Person
property.
It's wrong to say that Person
was expected to be the same string and it was expected to be a certain number :)
Also, Address
is another "complex" object, but the error message doesn't present the exact property that failed the assertion.
Ideally, to avoid excessive nesting, the error message could look like this:
Expected instance to match inspector, but the inspector was not satisfied:
Expected instance.Person.Name to be the same string, but they differ at index 1:
↓ (actual)
"Buford Howard Tannen"
"Biff Tannen"
↑ (expected).
Expected instance.Person.Age to be 48, but found 87 (difference of 39).
Expected instance.Address.City to be "Hill Valley, San Diego County, California" with a length of 41, but "Hill Valley" has a length of 11, differs near "y" (index 10).
I hope that makes sense :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to look into that (again). Later today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Crap, you're right. The call to new AssertionScope(CallerIdentifier.DetermineCallerIdentity())
doesn't actually help at all. Sorry about that. I thought I could fix that by changing some of the behavior AssertionScope
uses. But it turns out, it doesn't. So maybe we can rewrite Expected dto.Person to match inspector, but the inspector was not satisfied:
to be less verbose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please also note that
CallerIdentifier.DetermineCallerIdentity()
is very expensive to call and is only needed when the test fails.
True, but it's no longer relevant. My suggestion doesn't work, so the original implementation is still the best.
Pull Request Test Coverage Report for Build 8318031359Details
💛 - Coveralls |
Apparently not. |
@siewers Could you rebase to current develop and force push again? |
84bfdf1
to
a7be412
Compare
/// <exception cref="ArgumentNullException"><paramref name="expected"/> is <see langword="null"/>.</exception> | ||
public AndConstraint<TAssertions> Satisfy<T>(Action<T> expected) | ||
/// <exception cref="ArgumentNullException"><paramref name="assertion"/> is <see langword="null"/>.</exception> | ||
public AndConstraint<TAssertions> Satisfy<T>(Action<T> assertion) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think assertion
is a bit misleading here 🤔
(But feel free to disagree :) )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're supposed to use that action to build a composite assertion, so I think it's a suitable term.
Did you close it on purpose? |
Wow, no, I didn't... I don't know what happened. |
Execute.Assertion | ||
.ForCondition(Subject is T) | ||
.WithDefaultIdentifier(Identifier) | ||
.FailWith("Expected {context:object} to be assignable to {0}{reason}, but {1} is not.", typeof(T), Subject.GetType()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If Subject
is null, Subject.GetType()
throws an NRE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a check for null, although the message is almost duplicated.
I also used .Then
to chain the calls, but I'm not sure that does the same thing as you've suggested using the result of the first assertion to execute the next.
|
||
using (var assertionScope = new AssertionScope()) | ||
{ | ||
assertion((T)Subject); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If Satisfy
is used inside an AssertionScope
, then the execution won't stop if Subject is T
above fails, meaning we would send in a null
to action
.
object subject = null;
Action act = () =>
{
using var _ = new AssertionScope();
subject.Should().Satisfy<object>(x => x.Should().NotBeNull());
};
What we usually do to guard against this.
bool success = Execute.Assertion
.ForCondition(...)
.FailWith(...);
if (success)
{
Execute.Assertion
.ForCondition(...)
.FailWith(...);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think I understand exactly what you mean.
Is it about using an assertion scope within Satisfy
like this:
personAndAddressDto.Should().Satisfy<PersonAndAddressDto>(dto => {
dto.Person.Name.Should().NotBeNullOrEmpty();
using(new AssertionScope())
{
dto.Address.Should().Satisfy<AddressDto>(address => address.City.Should().NotBeNullOrEmpty());
}
}
After I added the null-check, it seems to work as I would expect.
I'm not sure why anyone would add a nested assertion scope though, but of course it needs to be handled correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What @jnyrup means is that if you're new Satisfy
is wrapped in an AssertionScope
(just like you do yourself), the FailWith
call with not throw an exception and return false
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm so sorry for the late reply. I will try to support that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this test verify the expected behavior (it passes with the current implementation)?
[Fact]
public void Using_assertion_scope_with_null_subject()
{
// Arrange
object subject = null;
// Act
Action act = () =>
{
using (new AssertionScope())
{
subject.Should().Satisfy<object>(x => x.Should().NotBeNull());
}
};
// Assert
act.Should().Throw<XunitException>()
.WithMessage("Expected subject to be assignable to System.Object, but found <null>.");
}
If this is not what you mean, I need some help understanding the scenario you have in mind.
Co-authored-by: Dennis Doomen <dennis.doomen@avivasolutions.nl>
/// <param name="assertion">The element inspector which must be satisfied by the <typeparamref name="TSubject" />.</param> | ||
/// <returns>An <see cref="AndConstraint{T}" /> which can be used to chain assertions.</returns> | ||
/// <exception cref="ArgumentNullException"><paramref name="assertion"/> is <see langword="null"/>.</exception> | ||
public AndConstraint<TAssertions> Satisfy<T>(Action<T> assertion) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this also have string because = "", params object[] becauseArgs
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might, but I don't know how to connect that to the nested assertions or how the resulting assertion message should look.
In practice, the nested assertions can have the because
, but the parent would probably always have the message "because the assertions should be satisfied" or something like that.
Added support for Satisfy on ReferenceTypeAssertions allowing to use element inspectors as an alternative to using predicates on Match.
Example:
IMPORTANT
./build.sh --target spellcheck
or.\build.ps1 --target spellcheck
before pushing and check the good outcome