fluentassertions / fluentassertions

A very extensive set of extension methods that allow you to more naturally specify the expected outcome of a TDD or BDD-style unit tests. Targets .NET Framework 4.7, as well as .NET Core 2.1, .NET Core 3.0, .NET 6, .NET Standard 2.0 and 2.1. Supports the unit test frameworks MSTest2, NUnit3, XUnit2, MSpec, and NSpec3.
https://www.fluentassertions.com
Apache License 2.0
3.77k stars 548 forks source link

How to exclude members from BeEquivalentTo that are null #1086

Closed ghost closed 5 years ago

ghost commented 5 years ago

Hi,

I have code like this:

var user = table.CreateInstance<User>();
userDTO.Should().BeEquivalentTo(user, options => options.ExcludingMissingMembers());

user: obraz

example userDTO:

{ "email": "testuser01@kl;kl;.com", "password": "nn91PKBrv99yQc/uNqTM2", "id": 2, "name": "Ervin Howell", "username": "Antonette", "address": { "street": "Victor Plains", "suite": "Suite 879", "city": "Wisokyburgh", "zipcode": "90566-7771", "geo": { "lat": "-43.9509", "lng": "-34.4618" } }, "phone": "010-692-6593 x09125", "website": "anastasia.net", "company": { "name": "Deckow-Crist", "catchPhrase": "Proactive didactic contingency", "bs": "synergize scalable supply-chains" } },

I want assert only fields that are not null in user but get exception:

'Expected member Email to be <null>, but found "testuser01@dfhdfh.com". Expected member Password to be <null>, but found "O5jG6//tFKinn91PKBrv99yQc/uNqTM2". Expected member Company to be <null>, but found

Null values still are checked. How to not check null values?

dennisdoomen commented 5 years ago

To be honest, I think it's a scary way to compare two types. Just imagine what happens if Username suddenly become null. But if your intention is to compare only certain properties, you can pass in an anonymous type instead of user that only defines the properties that you care about.

ghost commented 5 years ago

user object is building from my BDD table so I know what fields I want to assert with userDTO object

Then The user should have the following values
        | Key      | Value               |
        | id       | 2                   |
        | name     | Ervin Howell        |
        | username | Antonette           |
        | phone    | 010-692-6593 x09125 |
        | website  | anastasia.net       |

"you can pass in an anonymous type instead of user that only defines the properties that you care about." what do you mean?

dennisdoomen commented 5 years ago
userDTO.Should().BeEquivalentTo(new 
{
   Key = "Value",
   id = 2,
   name = "Ervin Howell",
   username = "Antonette",
   phone = "010-692-6593 x09125",
   website = "anastasia.net"
});
ghost commented 5 years ago

It is not the way I want to use it.

jnyrup commented 5 years ago

It seems to me we're having a XY-problem (I think the quote is clear, despite it being slightly pejorative)

  • User wants to do X.
  • User doesn't know how to do X, but thinks they can fumble their way to a solution if they can just manage to do Y.
  • User doesn't know how to do Y either.
  • User asks for help with Y.
  • Others try to help user with Y, but are confused because Y seems like a strange problem to want to solve.
  • After much interaction and wasted time, it finally becomes clear that the user really wants help with X, and that Y wasn't even a suitable solution for X.

We're advising against designing tests to exclude member based on their runtime values. That's a recipe for fragile tests. E.g. the test breaks if:

Without knowing the entire context of your setup, I think Dennis' example above is the best way to write the test as it is entirely clear what userDTO is expected to be.

user object is building from my BDD table so I know what fields I want to assert with userDTO object Then how about this one. It's a bit longer, but it:

  • re-uses the values from CreateInstance instead of duplicating it
  • Specifies exactly what members to compares userDTO against
var user = table.CreateInstance<User>();
var expected = new
{
   Key = user.Key,
   id = user.id,
   name = user.name,
   username = user.username,
   phone = user.phone,
   website = user.website
}

userDTO.should().BeEquivalentTo(expected);
ghost commented 5 years ago

ExcludingMissingMembers() is not working as expected. Should not compare values if field in expected object is null or missing.

I dont want hardcode new object with null fields in my method. I have user object as expected with possibility with null fields and userDTO object. I want compare non null fields from user object with the same fields in userDTO object

That is all, without creating new object.

var user = table.CreateInstance<User>(); is for create expected object from BDD table, nothing wrong here

lg2de commented 5 years ago

@Haxy89 The existing behavior of FluentAssertions is correct for me. The member is not missing if it is null. I think your question will result into a new feature like ExcludingNullMembers.

ghost commented 5 years ago

@Ig2de Yes! This is what i need. I thought excludingmissingmember is in the scope of null member :)

dennisdoomen commented 5 years ago

No, when defining an expectation, a missing member is a property or field that the expectation does not have. Normally I would expect my test to fail if the value of a property on the expectation is null and the subject's property is not.

The only option you have is to implement a custom IMemberMatchingRule and add it using the Using method exposed through the options parameter.

jnyrup commented 5 years ago

Here's an example on how to recursively ignore members from the expectation, when their runtime value is null.

class User
{
    public string Name { get; set; }

    public string Address { get; set; }
}

class UserDTO
{
    public string Name { get; set; }

    public string Address { get; set; }
}

class IgnoreNullMembersInExpectation : IEquivalencyStep
{
    public bool CanHandle(IEquivalencyValidationContext context, IEquivalencyAssertionOptions config) => context.Expectation is null;

    public bool Handle(IEquivalencyValidationContext context, IEquivalencyValidator parent, IEquivalencyAssertionOptions config) => true;
}

[TestClass]
public class IgnoreNullMembers
{
    [TestMethod]
    public void User_and_UserDTO_only_differs_by_null_property()
    {
        // Arrange
        var userDTO = new UserDTO
        {
            Name = "foo",
            Address = "bar"
        };

        var user = new User
        {
            Name = "foo",
            Address = null
        };

        // Assert
        userDTO.Should().BeEquivalentTo(user, opt => opt
            .Using(new IgnoreNullMembersInExpectation()));
    }

    [TestMethod]
    public void User_and_UserDTO_differs_by_non_null_property()
    {
        // Arrange
        var userDTO = new UserDTO
        {
            Name = "foo",
            Address = "bar"
        };

        var user = new User
        {
            Name = "baz",
            Address = null
        };

        // Assert
        userDTO.Should().NotBeEquivalentTo(user, opt => opt
            .Using(new IgnoreNullMembersInExpectation()));
    }
}
jnyrup commented 5 years ago

@Haxy89 Does the example above works for you?

jnyrup commented 5 years ago

Closing this issue. I've given an example implementation of an IEquivalencyStep. I don't foresee we would want include that implementation in Fluent Assertions.

tjuergens commented 3 years ago

Thanks for the IEquivalencyStep example - works like a charm, just what I needed.

bkqc commented 6 months ago
userDTO.Should().BeEquivalentTo(new 
{
   Key = "Value",
   id = 2,
   name = "Ervin Howell",
   username = "Antonette",
   phone = "010-692-6593 x09125",
   website = "anastasia.net"
});

The problem I got with this approach is with refactoring. Normally, in VisualStudio, we are able to do global renames but, with the anonymous object approach, it won't be capable of knowing that the property named username in there is the same as the one in the actual class and it won't be renamed ending in a nightmare of semi-manual replaces.

Would it be possible to have some way of using Expressions maybe?

userDTO.Should().BeEquivalentTo(usertDTOType x =>new Dictionary<Expression, object>(){
  {() => x.id, 2},
  {() => x.name, "Ervin Howell"},
  [...]
}

I'm really not sure about the syntax as I'm always mixed up with Expressions but I hope the intent is clear. Am I the only one having this concern? If so, how do you manage? If not, maybe I should file a feature request...

dennisdoomen commented 6 months ago

The problem I got with this approach is with refactoring. Normally, in VisualStudio, we are able to do global renames but, with the anonymous object approach, it won't be capable of knowing that the property named username in there is the same as the one in the actual class and it won't be renamed ending in a nightmare of semi-manual replaces.

I assume that if you're using an anonymous type, you're verifying a contract. Renaming the property of the userDto as part of a refactoring sounds like a breaking change to me and should fail your tests.

bkqc commented 4 months ago

In general, you are totally right.

Sorry, I was thinking of my own specific case where I don't expose any public client API. I have an end user app and everything public is part of the same solution so refactoring is not, per se, a breaking change since the whole app is globally built each time hence my "problem".