如何断言所有选定的属性都已设置(非空或空)

时间:2013-12-28 14:17:35

标签: c# .net fluent-assertions

我想验证(断言)我的DTO对象上的某些属性已设置。 我试图用Fluent Assertions来做,但以下代码似乎不起作用:

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

是否可以通过Fluent Assertions或其他工具实现这一目标? Fluent断言具有ShouldBeEquivalentTo,但实际上我只关心那些是不是空/空,所以我无法利用它。

当然我可以在每个属性级别上做一个Assert,但对一些更优雅的方式感兴趣。

4 个答案:

答案 0 :(得分:5)

实际上,Properties方法返回PropertiesAssertion,它只有EqualTo方法进行相等比较。 NotEqualTo方法或NotNull。在您的测试中,您的预期PropertiesAssertion不是null,这就是为什么它总是会通过。

您可以实现AssertionHelper静态类并传递Func的数组,您将使用它来验证对象。这是非常天真的实现,你不会得到很好的错误报告,但我只是展示了一般的想法

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");
}

现在,此测试将失败并显示some of the properties are null消息

var myDto = new MyDto();

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

该解决方案有两个问题

  • 如果Id属性属于值类型,则getter(objectToInspect) == null始终为false
  • 您没有获取null属性的名称,只是一般消息。

要解决第一点,您可以:

  • CheckAllPropertiesAreNotNull创建一个重载,每个都有不同数量的Generic Func<TInput, TFirstOutput> firstGetter,然后您将每个getter的返回值与相应的default(TFirstOutput)
  • 进行比较
  • 使用Activator创建默认实例并调用Equals

我会告诉你第二个案例。您可以创建一个IsDefault方法,该方法接受object类型的参数(注意,这可能是一个盒装的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);
}

现在我们的整体代码,处理程序值类型将如下所示:

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");
}

要解决第二点,您可以传递Expression<Func<T, object>>[] getters,其中包含有关被叫属性的信息。创建一个方法GetName,它接受​​Expression<Func<T, object>>并返回被叫属性名称

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;
}

现在生成的代码如下:

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

现在给我打电话

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

我得到了

  

预计下一个属性不具有默认值:Description,Id

错误消息。在我的Dto中DescriptionstringId是值类型int。如果我将该属性设置为某些非默认值,我将不会收到任何错误,测试将通过。

答案 1 :(得分:2)

我已经花了一些时间来解决这个问题。 @Dennis提出的解决方案由于多种原因而无法正常工作,这太糟糕了,因为它比以下解决方案更接近,更清晰。 Dennis方法不起作用的主要原因是ReferenceEqualityEquivalencyStep在应用断言规则之前处理空值。第二个原因是通过使用.When(info =&gt; true)我们删除了测试嵌套属性和数组元素的能力。解决这个问题的方法就像是.When(info =&gt;!info.RuntimeType.IsComplexType()&amp;&amp;!(info.RuntimeType属于IEnumerable类型)),但这只应该在值为测试不是空的。问题是ISubjecInfo不允许访问当前主题,因此虽然等效步骤在决定它是否可以处理时可以访问主题,但断言规则却没有。

无论如何,这是我解决问题的方法。我很可能没有想到一切。

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

    }
}

以下是对它的测试:

[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)
" );
}

答案 2 :(得分:0)

您可以使用`ShouldBeEquivalentTo'API在所有类型的属性上添加覆盖,如下所示:

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

然后谓词确定哪些属性应该使用此自定义断言。

答案 3 :(得分:0)

尝试一下:

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