How to assert that all selected properties are set (not null or empty)
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 Func
s, 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 alwaysfalse
- 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 GenericFunc<TInput, TFirstOutput> firstGetter
, then you would compare return value of each getter to correspondingdefault(TFirstOutput)
- use
Activator
, to create default instance and callEquals
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.
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)
" );
}