How to assert that all selected properties are set (not null or empty)

TarasB picture TarasB · Dec 28, 2013 · Viewed 12.5k times · Source

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.

Answer

Ilya Ivanov picture Ilya Ivanov · Dec 28, 2013

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.