简体   繁体   English

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

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

I want to verify (assert) that certain properties on my DTO object are set.我想验证(断言)我的 DTO 对象上的某些属性是否已设置。 I was trying to do it with Fluent Assertions, but the following code does not seem to work:我试图用 Fluent Assertions 做到这一点,但以下代码似乎不起作用:

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 或其他工具来实现? Fluent assertions have ShouldBeEquivalentTo, but actually I only care whether those are not nulls/empties, so that one I was not able to utilize. Fluent 断言有 ShouldBeEquivalentTo,但实际上我只关心它们是否不是空值/空值,因此我无法使用。

Of course I can just do an Assert on each property level, but interested in some more elegant way.当然,我可以在每个属性级别上做一个 Assert,但对一些更优雅的方式感兴趣。

Indeed, Properties method returns PropertiesAssertion , which only have EqualTo method for equality comparison.事实上, Properties方法返回PropertiesAssertion ,它只有EqualTo方法进行相等比较。 No NotEqualTo method or NotNull .没有NotEqualTo方法或NotNull In your test, your expected PropertiesAssertion not to be null , that's why it will always pass.在您的测试中,您预期的PropertiesAssertion不是null ,这就是它总是会通过的原因。

You can implement a AssertionHelper static class and pass an array of Func s, which you would use to validate an object.您可以实现一个AssertionHelper静态类并传递一个Func数组,您将使用它来验证一个对象。 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现在这个测试会失败, some of the properties are null消息

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如果Id属性是值类型,则getter(objectToInspect) == null始终为false
  • You don't get the names of the properties which was null, just a general message.您不会得到 null 属性的名称,只是一条一般消息。

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)创建CheckAllPropertiesAreNotNull的重载,每个都有不同数量的 Generic Func<TInput, TFirstOutput> firstGetter ,然后您将每个 getter 的返回值与相应的default(TFirstOutput)
  • use Activator , to create default instance and call Equals使用Activator ,创建默认实例并调用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):您可以创建一个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);
}

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.为了解决第二点,您可以传递一个Expression<Func<T, object>>[] getters ,它将包含有关被调用属性的信息。 Create a method GetName , which would accept Expression<Func<T, object>> and return the called property name创建一个方法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;
}

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预期下一个属性没有默认值:描述、ID

error message.错误信息。 In my Dto Description is a string and Id is a value type int .在我的 Dto Description是一个stringId是一个值类型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.由于多种原因,@Dennis 提出的解决方案无法正常工作,这太糟糕了,因为它与以下解决方法非常接近且干净得多。 The primary reason why Dennis method doesn't work is that the ReferenceEqualityEquivalencyStep handles null values, before the assertion rules are applied. Dennis 方法不起作用的主要原因是 ReferenceEqualityEquivalencyStep 在应用断言规则之前处理空值。 The second reason is that by using .When( info => true ) we remove the ability to test nested properties and array elements.第二个原因是,通过使用 .When( info => true ) 我们移除了测试嵌套属性和数组元素的能力。 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.解决这个问题的一种方法是 .When( info => !info.RuntimeType.IsComplexType() && !(info.RuntimeType is of type IEnumerable) ),但这应该只在被测试的值不为空时适用. 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.问题是 ISubjecInfo 不允许访问当前主题,因此当决定是否可以处理时,等效步骤可以访问主题,而断言规则则不允许。

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:您可以使用“ShouldBeEquivalentTo” API 在所有类型的属性上添加覆盖,如下所示:

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.然后When谓词确定哪些属性应该使用此自定义断言。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM