Question

I want to verify (assert) that certain properties on my DTO object are set. I was trying to do it with Fluent Assertions, but the following code does not seem to work:

mapped.ShouldHave().Properties(
    x => x.Description,
    ...more
    x => x.Id)
    .Should().NotBeNull(); 

Is it possible to achieve that with Fluent Assertions, or other tool ? Fluent assertions have ShouldBeEquivalentTo, but actually I only care whether those are not nulls/empties, so that one I was not able to utilize.

Of course I can just do an Assert on each property level, but interested in some more elegant way.

Was it helpful?

Solution

Indeed, Properties method returns PropertiesAssertion, which only have EqualTo method for equality comparison. No NotEqualTo method or NotNull. In your test, your expected PropertiesAssertion not to be null, that's why it will always pass.

You can implement a AssertionHelper static class and pass an array of Funcs, which you would use to validate an object. This is very naive implementation and you won't get nice error reporting, but I'm just showing the general idea

public static void CheckAllPropertiesAreNotNull<T>(this T objectToInspect, 
                                                params Func<T, object>[] getters)
{
    if (getters.Any(f => f(objectToInspect) == null))
        Assert.Fail("some of the properties are null");
}

Now this test would fail with some of the properties are null message

var myDto = new MyDto();

myDto.CheckAllPropertiesAreNotNull(x => x.Description, 
                                   x => x.Id); 

Two problems with that solution:

  • If Id property is of a value type, getter(objectToInspect) == null is always false
  • You don't get the names of the properties which was null, just a general message.

To address the first point, you can:

  • Create an overloads to CheckAllPropertiesAreNotNull, each will have different number of Generic Func<TInput, TFirstOutput> firstGetter, then you would compare return value of each getter to corresponding default(TFirstOutput)
  • use Activator, to create default instance and call Equals

I'll show you the second case. You can create a IsDefault method, which would accept parameter of type object (note, that this could be a boxed int):

private static bool IsDefault(this object value)
{
    if (value == null)
        return true;

    if (!value.GetType().IsValueType) //if a reference type and not null
        return false;

    //all value types should have a parameterless constructor
    var defaultValue = Activator.CreateInstance(value.GetType());
    return value.Equals(defaultValue);
}

Now our overall code, that handler value types will look like:

public static void CheckAllPropertiesAreNotDefault<T>(this T objectToInspect, 
                                                params Func<T, object>[] getters)
{
    if (getters.Any(f => f(objectToInspect).IsDefault()))
        Assert.Fail("some of the properties are not null");
}

To address the second point, you can pass an Expression<Func<T, object>>[] getters, which will contain information about a called Property. Create a method GetName, which would accept Expression<Func<T, object>> and return the called property name

public static string GetName<T>(Expression<Func<T, object>> exp)
{
    var body = exp.Body as MemberExpression;

    //return type is an object, so type cast expression will be added to value types
    if (body == null) 
    {
        var ubody = (UnaryExpression)exp.Body;
        body = ubody.Operand as MemberExpression;
    }

    return body.Member.Name;
}

Now the resulting code would look like:

public static void CheckAllPropertiesAreNotDefault<T>(this T objectToInspect, 
                                     params Expression<Func<T, object>>[] getters)
{
    var defaultProperties = getters.Where(f => f.Compile()(objectToInspect).IsDefault());

    if (defaultProperties.Any())
    {
        var commaSeparatedPropertiesNames = string.Join(", ", defaultProperties.Select(GetName));
        Assert.Fail("expected next properties not to have default values: " + commaSeparatedPropertiesNames);
    }
}

Now for my call

myDto.CheckAllPropertiesAreNotDefault(x => x.Description,
                                      x => x.Id);

I get

expected next properties not to have default values: Description, Id

error message. In my Dto Description is a string and Id is a value type int. If I set that properties to some non-default values, I'll get no error and test will pass.

OTHER TIPS

I've been speding some time on this problem. The solution proposed by @Dennis doesn't work correctly for a number of reasons, which is too bad because it is so close and a lot cleaner than the following workaround. The primary reason why Dennis method doesn't work is that the ReferenceEqualityEquivalencyStep handles null values, before the assertion rules are applied. The second reason is that by using .When( info => true ) we remove the ability to test nested properties and array elements. A way to get around that would be something like .When( info => !info.RuntimeType.IsComplexType() && !(info.RuntimeType is of type IEnumerable) ), but that should only apply when the value being tested is not null. The problem is that ISubjecInfo doesn't allow acces to the current subject, so while equivalency steps have access to the subject when deciding if it can handle, assertion rules do not.

Anyways here is my solution to the problem. It is very possible that I haven't thought of everything.

namespace FluentAssertions
{
   public class SimpleIsNotDefaultEquivalencyStep : IEquivalencyStep
   {
        public bool CanHandle(EquivalencyValidationContext context, IEquivalencyAssertionOptions config)
        {
            return true;
        }

        public virtual bool Handle(EquivalencyValidationContext context, IEquivalencyValidator structuralEqualityValidator, IEquivalencyAssertionOptions config)
        {
            context.Subject.Should().NotBeDefault( context.Reason, context.ReasonArgs );
            return true;
        }
    }



    public static class FluentAssertionsDefaultnessExtensions
    {
        private static bool IsDefault( object value, bool orValueTypeDefault = false )
        {
            if( value == null )
            {
                return true;
            }

            Type t = value.GetType();
            t = orValueTypeDefault ? Nullable.GetUnderlyingType( t ) ?? t : t;

            if( t.IsValueType )
            {
                object defaultValue = Activator.CreateInstance( t );
                return value.Equals( defaultValue );
            }
            else if( value is string )
            {
                return string.IsNullOrWhiteSpace( value as string );
            }

            return false;
        }

        private static bool IsDefaultOrValueTypeDefault( object value )
        {
            return IsDefault( value, orValueTypeDefault: true );
        }

        public static AndConstraint<ObjectAssertions> NotBeDefault( this ObjectAssertions assertions, string because = "", params object[] reasonArgs )
        {
            Execute.Assertion
                .BecauseOf( because, reasonArgs )
                .ForCondition( !IsDefault( assertions.Subject ) )
                .FailWith( "Expected {context:object} to not be default{reason}, but found {0}.", assertions.Subject );

            return new AndConstraint<ObjectAssertions>( assertions );
        }

        public static AndConstraint<StringAssertions> NotBeDefault( this StringAssertions assertions, string because = "", params object[] reasonArgs )
        {
            Execute.Assertion
                .BecauseOf( because, reasonArgs )
                .ForCondition( !IsDefault( assertions.Subject ) )
                .FailWith( "Expected {context:object} to not be default{reason}, but found {0}.", assertions.Subject );

            return new AndConstraint<StringAssertions>( assertions );
        }

        public static AndConstraint<Numeric.NumericAssertions<T>> NotBeDefault<T>( this Numeric.NumericAssertions<T> assertions, string because = "", params object[] reasonArgs ) where T : struct
        {
            Execute.Assertion
                .BecauseOf( because, reasonArgs )
                .ForCondition( !IsDefault( assertions.Subject ) )
                .FailWith( "Expected {context:object} to not be default{reason}, but found {0}.", assertions.Subject );

            return new AndConstraint<Numeric.NumericAssertions<T>>( assertions );
        }

        public static AndConstraint<BooleanAssertions> NotBeDefault( this BooleanAssertions assertions, string because = "", params object[] reasonArgs )
        {
            Execute.Assertion
                .BecauseOf( because, reasonArgs )
                .ForCondition( !IsDefault( assertions.Subject ) )
                .FailWith( "Expected {context:object} to not be default{reason}, but found {0}.", assertions.Subject );

            return new AndConstraint<BooleanAssertions>( assertions );
        }

        public static AndConstraint<GuidAssertions> NotBeDefault( this GuidAssertions assertions, string because = "", params object[] reasonArgs )
        {
            Execute.Assertion
                .BecauseOf( because, reasonArgs )
                .ForCondition( !IsDefault( assertions.Subject ) )
                .FailWith( "Expected {context:object} to not be default{reason}, but found {0}.", assertions.Subject );

            return new AndConstraint<GuidAssertions>( assertions );
        }


        public static void ShouldNotBeEquivalentToDefault<T>( this T subject, string because = "", params object[] reasonArgs )
        {
            ShouldNotBeEquivalentToDefault( subject, config => config, because, reasonArgs );
        }

        public static void ShouldNotBeEquivalentToDefault<T>( this T subject, 
            Func<EquivalencyAssertionOptions<T>, EquivalencyAssertionOptions<T>> config, string because = "", params object[] reasonArgs )
        {
            var context = new EquivalencyValidationContext
            {
                Subject = subject,
                Expectation = subject,
                CompileTimeType = typeof( T ),
                Reason = because,
                ReasonArgs = reasonArgs
            };

            var validator = new EquivalencyValidator( 
                config( EquivalencyAssertionOptions<T>.Default()
                    .Using<string>( ctx => ctx.Subject.Should().NotBeDefault() ).WhenTypeIs<string>() )
                    .WithStrictOrdering()
                    );

            validator.Steps.Remove( validator.Steps.Single( _ => typeof( TryConversionEquivalencyStep ) == _.GetType() ) );
            validator.Steps.Remove( validator.Steps.Single( _ => typeof( ReferenceEqualityEquivalencyStep ) == _.GetType() ) );
            validator.Steps.Remove( validator.Steps.Single( _ => typeof( SimpleEqualityEquivalencyStep ) == _.GetType() ) );

            validator.Steps.Add( new SimpleIsNotDefaultEquivalencyStep() );

            validator.AssertEquality( context );
        }

    }
}

Here is a test for it:

[TestMethod]
[TestCategory( TestCategory2 )]
public void Test_NotBeDefault()
{
    ((Action)(() => ((int?)null).Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because null is default for int?" );
    ((Action)(() => ((int?)0).Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because 0 is value type default for int?" );
    ((Action)(() => 0.Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because null is value type default for int" );
    ((Action)(() => ((int?)1).Should().NotBeDefault())).ShouldNotThrow( "because 1 is not default for int?" );
    ((Action)(() => 1.Should().NotBeDefault())).ShouldNotThrow( "because 1 is not default for int" );

    ((Action)(() => ((object)null).Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because null is default for object" );
    ((Action)(() => ((object)new object()).Should().NotBeDefault())).ShouldNotThrow( "because not null is not default for object" );

    ((Action)(() => ((string)null).Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because null is default for string" );
    ((Action)(() => ((string)"").Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because empty string is default for string" );
    ((Action)(() => ((string)"hi").Should().NotBeDefault())).ShouldNotThrow( "because \"hi\" is not default for string" );

    ((Action)(() => ((bool?)null).Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because null is default for bool?" );
    ((Action)(() => ((bool?)false).Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because false is default for bool?" );
    ((Action)(() => false.Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because false is default for bool" );
    ((Action)(() => ((bool?)true).Should().NotBeDefault())).ShouldNotThrow( "because true is not default for bool?" );
    ((Action)(() => true.Should().NotBeDefault())).ShouldNotThrow( "because true is not default for bool" );

    var actual = new
    {
        i1 = (int?)null,
        i2 = (int?)0,
        i3 = 0,
        i4 = (int?)1,
        i5 = 1,

        s1 = (string)null,
        s2 = (string)"",
        s3 = (string)"hi",

        b1 = (bool?)null,
        b2 = (bool?)false,
        b3 = false,
        b4 = (bool?)true,
        b5 = true,

        n1 = (PlainClass)null,
        n2 = new PlainClass(),
        n3 = new PlainClass
        {
            Key = 10,
            NestedProperty = new object()
        },

        a1 = (PlainClass[])null,
        a2 = new [] { "", "hi", null },
        a3 = new [] { 0, 11 },
        a4 = new [] { new PlainClass { Key = 42 } },

        g1 = (Guid?)null,
        g2 = (Guid)Guid.Empty,
        g3 = Guid.NewGuid()
    };

    ((Action)(() => actual.ShouldNotBeEquivalentToDefault())).ShouldThrow<AssertFailedException>().WithMessage(
@"Expected property i1 to not be default, but found <null>.
Expected property i2 to not be default, but found 0.
Expected property i3 to not be default, but found 0.
Expected property s1 to not be default, but found <null>.
Expected property s2 to not be default, but found """".
Expected property b1 to not be default, but found <null>.
Expected property b2 to not be default, but found False.
Expected property b3 to not be default, but found False.
Expected property n1 to not be default, but found <null>.
Expected property n2.Key to not be default, but found 0.
Expected property n2.NestedProperty to not be default, but found <null>.
Expected property a1 to not be default, but found <null>.
Expected property a2[0] to not be default, but found """".
Expected property a2[2] to not be default, but found <null>.
Expected property a3[0] to not be default, but found 0.
Expected property a4[0].NestedProperty to not be default, but found <null>.
Expected property g1 to not be default, but found <null>.
Expected property g2 to not be default, but found {00000000-0000-0000-0000-000000000000}.

With configuration:
- Select all declared properties
- Match property by name (or throw)
- Invoke Action<String> when info.RuntimeType.IsSameOrInherits(System.String)
- Invoke Action<String> when info.RuntimeType.IsSameOrInherits(System.String)
" );
}

You could add an override on all types of properties using the `ShouldBeEquivalentTo' API like this:

mapped.ShouldBeEquivalentTo(mapped, options => options
   .ExcludingNestedObjects()
   .Using<object>(ctx => ctx.Subject.Should().NotBeNull())
   .When(info => true));

Then When predicate determines what properties should use this custom assertion.

try this:

 var mySelectedProperties = new string[] { "Description", "Id", "other" };
 Assert.IsTrue(mapped.GetType().GetProperties().ToList().
            Where(p => mySelectedProperties.Contains(p.Name)).
            Where(p => p.GetValue(mapped) == null).
            Count() == 0);
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top