【问题标题】:How to assert that all selected properties are set (not null or empty)如何断言所有选定的属性都已设置(不为空或为空)
【发布时间】:2014-01-15 22:11:06
【问题描述】:

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

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

是否可以使用 Fluent Assertions 或其他工具来实现? 流利的断言有 ShouldBeEquivalentTo,但实际上我只关心它们是否不是空值/空值,因此我无法使用。

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

【问题讨论】:

  • 请澄清empty 是什么意思?我在回答中给出的默认检查是否足够?
  • 我的意思是值与default 不同,对。

标签: c# .net fluent-assertions


【解决方案1】:

确实,Properties 方法返回PropertiesAssertion,它只有EqualTo 方法用于相等比较。 NotEqualTo 方法或NotNull。在您的测试中,您期望的PropertiesAssertion 不是null,这就是它总是通过的原因。

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

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
  • 您没有得到空属性的名称,只是一般消息。

要解决第一点,您可以:

  • CheckAllPropertiesAreNotNull创建一个重载,每个都有不同数量的Generic Func&lt;TInput, TFirstOutput&gt; 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&lt;Func&lt;T, object&gt;&gt;[] getters,其中将包含有关被调用属性的信息。创建一个方法GetName,它将接受Expression&lt;Func&lt;T, object&gt;&gt;并返回被调用的属性名称

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

我明白了

预计下一个属性没有默认值:描述、Id

错误信息。在我的 Dto 中,DescriptionstringId 是值类型 int。如果我将该属性设置为一些非默认值,则不会出现错误并且测试将通过。

【讨论】:

  • CheckAllPropertiesAreNotDefault 非常适合我的问题,谢谢。
【解决方案2】:

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

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

然后When 谓词决定哪些属性应该使用这个自定义断言。

【讨论】:

  • 您能具体说明一下这是如何工作的吗?举一些例子和错误信息。
  • 我试过了,好像不行。 试过了吗?
  • 它会给你什么错误?您是否尝试过使用 FA mapped.ShouldBeEquivalentTo(mapped, options => options .Using(ctx => ctx.Subject.Should().NotBeNull()) .When(info => true));2?
  • 获取 FA 2.2,然后尝试上面的示例。
  • 它不起作用的原因之一是 ReferenceEqualityEquivalencyStep 对于所有空值都返回 true。此外,通过使用 When( info => true ),不再可能检查嵌套属性和数组元素的默认值。
【解决方案3】:

我在这个问题上已经花了一些时间。 @Dennis 提出的解决方案由于多种原因无法正常工作,这太糟糕了,因为它比以下解决方法如此接近且干净得多。 Dennis 方法不起作用的主要原因是 ReferenceEqualityEquivalencyStep 在应用断言规则之前处理空值。第二个原因是通过使用 .When( info => true ) 我们删除了测试嵌套属性和数组元素的能力。解决此问题的方法类似于 .When( info => !info.RuntimeType.IsComplexType() && !(info.RuntimeType is of type 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)
" );
}

【讨论】:

    【解决方案4】:

    试试这个:

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

    【讨论】:

      猜你喜欢
      • 2018-11-22
      • 2021-08-25
      • 1970-01-01
      • 1970-01-01
      • 2014-05-06
      • 1970-01-01
      • 2021-01-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多