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

Add support for Satisfy on ReferenceTypeAssertions #2597

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from

Conversation

siewers
Copy link

@siewers siewers commented Feb 29, 2024

Added support for Satisfy on ReferenceTypeAssertions allowing to use element inspectors as an alternative to using predicates on Match.

Example:

var myDto = new Dto { Name = "Some Name" };

myDto.Should().Satisfy<Dto>(dto => dto.Name.Should().Be("Some Name"));

IMPORTANT

  • If the PR touches the public API, the changes have been approved in a separate issue with the "api-approved" label.
  • The code complies with the Coding Guidelines for C#.
  • The changes are covered by unit tests which follow the Arrange-Act-Assert syntax and the naming conventions such as is used in these tests.
  • If the PR adds a feature or fixes a bug, please update the release notes with a functional description that explains what the change means to consumers of this library, which are published on the website.
  • If the PR changes the public API the changes needs to be included by running AcceptApiChanges.ps1 or AcceptApiChanges.sh.
  • If the PR affects the documentation, please include your changes in this pull request so the documentation will appear on the website.
    • Please also run ./build.sh --target spellcheck or .\build.ps1 --target spellcheck before pushing and check the good outcome

Copy link

github-actions bot commented Feb 29, 2024

Qodana for .NET

It 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
☁️ View the detailed Qodana report

Contact Qodana team

Contact us at qodana-support@jetbrains.com

@ITaluone
Copy link
Contributor

ITaluone commented Mar 1, 2024

Missing release notes?

@siewers
Copy link
Author

siewers commented Mar 1, 2024

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?

@ITaluone
Copy link
Contributor

ITaluone commented Mar 1, 2024

I didn't add it to the release notes yet as I'm unsure what version this will be in.

I think while on develop for the next version it doesn't really matter

@siewers
Copy link
Author

siewers commented Mar 1, 2024

@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.

@siewers
Copy link
Author

siewers commented Mar 1, 2024

@dennisdoomen @ITaluone I believe I've addressed all comments now.

Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs Outdated Show resolved Hide resolved
Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs Outdated Show resolved Hide resolved
failuresFromInspector = assertionScope.Discard();
}

if (failuresFromInspector.Length > 0)
Copy link
Member

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?

Copy link
Author

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 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And?

Copy link
Author

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).

Copy link
Member

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?

Copy link
Author

@siewers siewers Mar 23, 2024

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 :)

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.
See #1660 and #1657

Copy link
Member

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.

docs/_pages/basicassertions.md Show resolved Hide resolved
docs/_pages/releases.md Outdated Show resolved Hide resolved
@coveralls
Copy link

coveralls commented Mar 11, 2024

Pull Request Test Coverage Report for Build 8318031359

Details

  • 17 of 17 (100.0%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.004%) to 97.537%

Totals Coverage Status
Change from base Build 8236406738: 0.004%
Covered Lines: 11961
Relevant Lines: 12145

💛 - Coveralls

@dennisdoomen
Copy link
Member

I believe I've addressed all comments now.

Apparently not.

@ITaluone
Copy link
Contributor

@siewers Could you rebase to current develop and force push again?

@siewers siewers force-pushed the develop branch 2 times, most recently from 84bfdf1 to a7be412 Compare March 12, 2024 13:44
/// <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)
Copy link
Contributor

@IT-VBFK IT-VBFK Mar 12, 2024

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 :) )

Copy link
Member

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.

@siewers siewers closed this Mar 17, 2024
@dennisdoomen
Copy link
Member

Did you close it on purpose?

@siewers
Copy link
Author

siewers commented Mar 17, 2024

Wow, no, I didn't... I don't know what happened.

@siewers siewers reopened this Mar 17, 2024
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());
Copy link
Member

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.

Copy link
Author

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);
Copy link
Member

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(...);
}

Copy link
Author

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.

Copy link
Member

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.

Copy link
Author

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.

Copy link
Author

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.

/// <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)
Copy link
Member

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?

Copy link
Author

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.

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

Successfully merging this pull request may close these issues.

None yet

6 participants