[英]How to determine if all properties of type List<T> in an object are null or empty?
[英]How to assert that all selected properties are set (not null or empty)
我想驗證(斷言)我的 DTO 對象上的某些屬性是否已設置。 我試圖用 Fluent Assertions 做到這一點,但以下代碼似乎不起作用:
mapped.ShouldHave().Properties(
x => x.Description,
...more
x => x.Id)
.Should().NotBeNull();
是否可以使用 Fluent Assertions 或其他工具來實現? Fluent 斷言有 ShouldBeEquivalentTo,但實際上我只關心它們是否不是空值/空值,因此我無法使用。
當然,我可以在每個屬性級別上做一個 Assert,但對一些更優雅的方式感興趣。
事實上, 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
要解決第一點,您可以:
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);
我得到
預期下一個屬性沒有默認值:描述、ID
錯誤信息。 在我的 Dto Description
是一個string
而Id
是一個值類型int
。 如果我將該屬性設置為一些非默認值,則不會出錯並且測試將通過。
我一直在加速解決這個問題。 由於多種原因,@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)
" );
}
您可以使用“ShouldBeEquivalentTo” API 在所有類型的屬性上添加覆蓋,如下所示:
mapped.ShouldBeEquivalentTo(mapped, options => options
.ExcludingNestedObjects()
.Using<object>(ctx => ctx.Subject.Should().NotBeNull())
.When(info => true));
然后When謂詞確定哪些屬性應該使用此自定義斷言。
試試這個:
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.